-
Campbell Barton authoredCampbell Barton authored
animation_motion_trail.py 73.46 KiB
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
# <pep8 compliant>
bl_info = {
'name': "Motion Trail",
'author': "Bart Crouch",
'version': (3, 1, 0),
'blender': (2, 6, 0),
'location': "View3D > Toolbar > Motion Trail tab",
'warning': "",
'description': "Display and edit motion trails in the 3d-view",
'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.5/"\
"Py/Scripts/Animation/Motion_Trail",
'tracker_url': "http://projects.blender.org/tracker/index.php?"\
"func=detail&aid=26374",
'category': 'Animation'}
import bgl
import blf
import bpy
from bpy_extras import view3d_utils
import math
import mathutils
# fake fcurve class, used if no fcurve is found for a path
class fake_fcurve():
def __init__(self, object, index, rotation=False, scale=False):
# location
if not rotation and not scale:
self.loc = object.location[index]
# scale
elif scale:
self.loc = object.scale[index]
# rotation
elif rotation == 'QUATERNION':
self.loc = object.rotation_quaternion[index]
elif rotation == 'AXIS_ANGLE':
self.loc = object.rotation_axis_angle[index]
else:
self.loc = object.rotation_euler[index]
self.keyframe_points = []
def evaluate(self, frame):
return(self.loc)
def range(self):
return([])
# get location curves of the given object
def get_curves(object, child=False):
if object.animation_data and object.animation_data.action:
action = object.animation_data.action
if child:
# posebone
curves = [fc for fc in action.fcurves if len(fc.data_path)>=14 \
and fc.data_path[-9:]=='.location' and \
child.name in fc.data_path.split("\"")]
else:
# normal object
curves = [fc for fc in action.fcurves if \
fc.data_path == 'location']
elif object.animation_data and object.animation_data.use_nla:
curves = []
strips = []
for track in object.animation_data.nla_tracks:
not_handled = [s for s in track.strips]
while not_handled:
current_strip = not_handled.pop(-1)
if current_strip.action:
strips.append(current_strip)
if current_strip.strips:
# meta strip
not_handled += [s for s in current_strip.strips]
for strip in strips:
if child:
# posebone
curves = [fc for fc in strip.action.fcurves if \
len(fc.data_path)>=14 and fc.data_path[-9:]=='.location' \
and child.name in fc.data_path.split("\"")]
else:
# normal object
curves = [fc for fc in strip.action.fcurves if \
fc.data_path == 'location']
if curves:
# use first strip with location fcurves
break
else:
# should not happen?
curves = []
# ensure we have three curves per object
fcx = None
fcy = None
fcz = None
for fc in curves:
if fc.array_index == 0:
fcx = fc
elif fc.array_index == 1:
fcy = fc
elif fc.array_index == 2:
fcz = fc
if fcx == None:
fcx = fake_fcurve(object, 0)
if fcy == None:
fcy = fake_fcurve(object, 1)
if fcz == None:
fcz = fake_fcurve(object, 2)
return([fcx, fcy, fcz])
# turn screen coordinates (x,y) into world coordinates vector
def screen_to_world(context, x, y):
depth_vector = view3d_utils.region_2d_to_vector_3d(\
context.region, context.region_data, [x,y])
vector = view3d_utils.region_2d_to_location_3d(\
context.region, context.region_data, [x,y], depth_vector)
return(vector)
# turn 3d world coordinates vector into screen coordinate integers (x,y)
def world_to_screen(context, vector):
prj = context.region_data.perspective_matrix * \
mathutils.Vector((vector[0], vector[1], vector[2], 1.0))
width_half = context.region.width / 2.0
height_half = context.region.height / 2.0
x = int(width_half + width_half * (prj.x / prj.w))
y = int(height_half + height_half * (prj.y / prj.w))
# correction for corner cases in perspective mode
if prj.w < 0:
if x < 0:
x = context.region.width * 2
else:
x = context.region.width * -2
if y < 0:
y = context.region.height * 2
else:
y = context.region.height * -2
return(x, y)
# calculate location of display_ob in worldspace
def get_location(frame, display_ob, offset_ob, curves):
if offset_ob:
bpy.context.scene.frame_set(frame)
display_mat = getattr(display_ob, "matrix", False)
if not display_mat:
# posebones have "matrix", objects have "matrix_world"
display_mat = display_ob.matrix_world
if offset_ob:
loc = display_mat.to_translation() + \
offset_ob.matrix_world.to_translation()
else:
loc = display_mat.to_translation()
else:
fcx, fcy, fcz = curves
locx = fcx.evaluate(frame)
locy = fcy.evaluate(frame)
locz = fcz.evaluate(frame)
loc = mathutils.Vector([locx, locy, locz])
return(loc)
# get position of keyframes and handles at the start of dragging
def get_original_animation_data(context, keyframes):
keyframes_ori = {}
handles_ori = {}
if context.active_object and context.active_object.mode == 'POSE':
armature_ob = context.active_object
objects = [[armature_ob, pb, armature_ob] for pb in \
context.selected_pose_bones]
else:
objects = [[ob, False, False] for ob in context.selected_objects]
for action_ob, child, offset_ob in objects:
if not action_ob.animation_data:
continue
curves = get_curves(action_ob, child)
if len(curves) == 0:
continue
fcx, fcy, fcz = curves
if child:
display_ob = child
else:
display_ob = action_ob
# get keyframe positions
frame_old = context.scene.frame_current
keyframes_ori[display_ob.name] = {}
for frame in keyframes[display_ob.name]:
loc = get_location(frame, display_ob, offset_ob, curves)
keyframes_ori[display_ob.name][frame] = [frame, loc]
# get handle positions
handles_ori[display_ob.name] = {}
for frame in keyframes[display_ob.name]:
handles_ori[display_ob.name][frame] = {}
left_x = [frame, fcx.evaluate(frame)]
right_x = [frame, fcx.evaluate(frame)]
for kf in fcx.keyframe_points:
if kf.co[0] == frame:
left_x = kf.handle_left[:]
right_x = kf.handle_right[:]
break
left_y = [frame, fcy.evaluate(frame)]
right_y = [frame, fcy.evaluate(frame)]
for kf in fcy.keyframe_points:
if kf.co[0] == frame:
left_y = kf.handle_left[:]
right_y = kf.handle_right[:]
break
left_z = [frame, fcz.evaluate(frame)]
right_z = [frame, fcz.evaluate(frame)]
for kf in fcz.keyframe_points:
if kf.co[0] == frame:
left_z = kf.handle_left[:]
right_z = kf.handle_right[:]
break
handles_ori[display_ob.name][frame]["left"] = [left_x, left_y,
left_z]
handles_ori[display_ob.name][frame]["right"] = [right_x, right_y,
right_z]
if context.scene.frame_current != frame_old:
context.scene.frame_set(frame_old)
return(keyframes_ori, handles_ori)
# callback function that calculates positions of all things that need be drawn
def calc_callback(self, context):
if context.active_object and context.active_object.mode == 'POSE':
armature_ob = context.active_object
objects = [[armature_ob, pb, armature_ob] for pb in \
context.selected_pose_bones]
else:
objects = [[ob, False, False] for ob in context.selected_objects]
if objects == self.displayed:
selection_change = False
else:
selection_change = True
if self.lock and not selection_change and \
context.region_data.perspective_matrix == self.perspective and not \
context.window_manager.motion_trail.force_update:
return
# dictionaries with key: objectname
self.paths = {} # value: list of lists with x, y, colour
self.keyframes = {} # value: dict with frame as key and [x,y] as value
self.handles = {} # value: dict of dicts
self.timebeads = {} # value: dict with frame as key and [x,y] as value
self.click = {} # value: list of lists with frame, type, loc-vector
if selection_change:
# value: editbone inverted rotation matrix or None
self.edit_bones = {}
if selection_change or not self.lock or context.window_manager.\
motion_trail.force_update:
# contains locations of path, keyframes and timebeads
self.cached = {"path":{}, "keyframes":{}, "timebeads_timing":{},
"timebeads_speed":{}}
if self.cached["path"]:
use_cache = True
else:
use_cache = False
self.perspective = context.region_data.perspective_matrix.copy()
self.displayed = objects # store, so it can be checked next time
context.window_manager.motion_trail.force_update = False
global_undo = context.user_preferences.edit.use_global_undo
context.user_preferences.edit.use_global_undo = False
for action_ob, child, offset_ob in objects:
if selection_change:
if not child:
self.edit_bones[action_ob.name] = None
else:
bpy.ops.object.mode_set(mode='EDIT')
editbones = action_ob.data.edit_bones
mat = editbones[child.name].matrix.copy().to_3x3().inverted()
bpy.ops.object.mode_set(mode='POSE')
self.edit_bones[child.name] = mat
if not action_ob.animation_data:
continue
curves = get_curves(action_ob, child)
if len(curves) == 0:
continue
if context.window_manager.motion_trail.path_before == 0:
range_min = context.scene.frame_start
else:
range_min = max(context.scene.frame_start,
context.scene.frame_current - \
context.window_manager.motion_trail.path_before)
if context.window_manager.motion_trail.path_after == 0:
range_max = context.scene.frame_end
else:
range_max = min(context.scene.frame_end,
context.scene.frame_current + \
context.window_manager.motion_trail.path_after)
fcx, fcy, fcz = curves
if child:
display_ob = child
else:
display_ob = action_ob
# get location data of motion path
path = []
speeds = []
frame_old = context.scene.frame_current
step = 11 - context.window_manager.motion_trail.path_resolution
if not use_cache:
if display_ob.name not in self.cached["path"]:
self.cached["path"][display_ob.name] = {}
if use_cache and range_min-1 in self.cached["path"][display_ob.name]:
prev_loc = self.cached["path"][display_ob.name][range_min-1]
else:
prev_loc = get_location(range_min-1, display_ob, offset_ob, curves)
self.cached["path"][display_ob.name][range_min-1] = prev_loc
for frame in range(range_min, range_max + 1, step):
if use_cache and frame in self.cached["path"][display_ob.name]:
loc = self.cached["path"][display_ob.name][frame]
else:
loc = get_location(frame, display_ob, offset_ob, curves)
self.cached["path"][display_ob.name][frame] = loc
if not context.region or not context.space_data:
continue
x, y = world_to_screen(context, loc)
if context.window_manager.motion_trail.path_style == 'simple':
path.append([x, y, [0.0, 0.0, 0.0], frame, action_ob, child])
else:
dloc = (loc - prev_loc).length
path.append([x, y, dloc, frame, action_ob, child])
speeds.append(dloc)
prev_loc = loc
# calculate colour of path
if context.window_manager.motion_trail.path_style == 'speed':
speeds.sort()
min_speed = speeds[0]
d_speed = speeds[-1] - min_speed
for i, [x, y, d_loc, frame, action_ob, child] in enumerate(path):
relative_speed = (d_loc - min_speed) / d_speed # 0.0 to 1.0
red = min(1.0, 2.0 * relative_speed)
blue = min(1.0, 2.0 - (2.0 * relative_speed))
path[i][2] = [red, 0.0, blue]
elif context.window_manager.motion_trail.path_style == 'acceleration':
accelerations = []
prev_speed = 0.0
for i, [x, y, d_loc, frame, action_ob, child] in enumerate(path):
accel = d_loc - prev_speed
accelerations.append(accel)
path[i][2] = accel
prev_speed = d_loc
accelerations.sort()
min_accel = accelerations[0]
max_accel = accelerations[-1]
for i, [x, y, accel, frame, action_ob, child] in enumerate(path):
if accel < 0:
relative_accel = accel / min_accel # values from 0.0 to 1.0
green = 1.0 - relative_accel
path[i][2] = [1.0, green, 0.0]
elif accel > 0:
relative_accel = accel / max_accel # values from 0.0 to 1.0
red = 1.0 - relative_accel
path[i][2] = [red, 1.0, 0.0]
else:
path[i][2] = [1.0, 1.0, 0.0]
self.paths[display_ob.name] = path
# get keyframes and handles
keyframes = {}
handle_difs = {}
kf_time = []
click = []
if not use_cache:
if display_ob.name not in self.cached["keyframes"]:
self.cached["keyframes"][display_ob.name] = {}
for fc in curves:
for kf in fc.keyframe_points:
# handles for location mode
if context.window_manager.motion_trail.mode == 'location':
if kf.co[0] not in handle_difs:
handle_difs[kf.co[0]] = {"left":mathutils.Vector(),
"right":mathutils.Vector(), "keyframe_loc":None}
handle_difs[kf.co[0]]["left"][fc.array_index] = \
(mathutils.Vector(kf.handle_left[:]) - \
mathutils.Vector(kf.co[:])).normalized()[1]
handle_difs[kf.co[0]]["right"][fc.array_index] = \
(mathutils.Vector(kf.handle_right[:]) - \
mathutils.Vector(kf.co[:])).normalized()[1]
# keyframes
if kf.co[0] in kf_time:
continue
kf_time.append(kf.co[0])
co = kf.co[0]
if use_cache and co in \
self.cached["keyframes"][display_ob.name]:
loc = self.cached["keyframes"][display_ob.name][co]
else:
loc = get_location(co, display_ob, offset_ob, curves)
self.cached["keyframes"][display_ob.name][co] = loc
if handle_difs:
handle_difs[co]["keyframe_loc"] = loc
x, y = world_to_screen(context, loc)
keyframes[kf.co[0]] = [x, y]
if context.window_manager.motion_trail.mode != 'speed':
# can't select keyframes in speed mode
click.append([kf.co[0], "keyframe",
mathutils.Vector([x,y]), action_ob, child])
self.keyframes[display_ob.name] = keyframes
# handles are only shown in location-altering mode
if context.window_manager.motion_trail.mode == 'location' and \
context.window_manager.motion_trail.handle_display:
# calculate handle positions
handles = {}
for frame, vecs in handle_difs.items():
if child:
# bone space to world space
mat = self.edit_bones[child.name].copy().inverted()
vec_left = vecs["left"] * mat
vec_right = vecs["right"] * mat
else:
vec_left = vecs["left"]
vec_right = vecs["right"]
if vecs["keyframe_loc"] != None:
vec_keyframe = vecs["keyframe_loc"]
else:
vec_keyframe = get_location(frame, display_ob, offset_ob,
curves)
x_left, y_left = world_to_screen(context, vec_left*2 + \
vec_keyframe)
x_right, y_right = world_to_screen(context, vec_right*2 + \
vec_keyframe)
handles[frame] = {"left":[x_left, y_left],
"right":[x_right, y_right]}
click.append([frame, "handle_left",
mathutils.Vector([x_left, y_left]), action_ob, child])
click.append([frame, "handle_right",
mathutils.Vector([x_right, y_right]), action_ob, child])
self.handles[display_ob.name] = handles
# calculate timebeads for timing mode
if context.window_manager.motion_trail.mode == 'timing':
timebeads = {}
n = context.window_manager.motion_trail.timebeads * (len(kf_time) \
- 1)
dframe = (range_max - range_min) / (n + 1)
if not use_cache:
if display_ob.name not in self.cached["timebeads_timing"]:
self.cached["timebeads_timing"][display_ob.name] = {}
for i in range(1, n+1):
frame = range_min + i * dframe
if use_cache and frame in \
self.cached["timebeads_timing"][display_ob.name]:
loc = self.cached["timebeads_timing"][display_ob.name]\
[frame]
else:
loc = get_location(frame, display_ob, offset_ob, curves)
self.cached["timebeads_timing"][display_ob.name][frame] = \
loc
x, y = world_to_screen(context, loc)
timebeads[frame] = [x, y]
click.append([frame, "timebead", mathutils.Vector([x,y]),
action_ob, child])
self.timebeads[display_ob.name] = timebeads
# calculate timebeads for speed mode
if context.window_manager.motion_trail.mode == 'speed':
angles = dict([[kf, {"left":[], "right":[]}] for kf in \
self.keyframes[display_ob.name]])
for fc in curves:
for i, kf in enumerate(fc.keyframe_points):
if i != 0:
angle = mathutils.Vector([-1, 0]).angle(mathutils.\
Vector(kf.handle_left) - mathutils.Vector(kf.co),
0)
if angle != 0:
angles[kf.co[0]]["left"].append(angle)
if i != len(fc.keyframe_points) - 1:
angle = mathutils.Vector([1, 0]).angle(mathutils.\
Vector(kf.handle_right) - mathutils.Vector(kf.co),
0)
if angle != 0:
angles[kf.co[0]]["right"].append(angle)
timebeads = {}
kf_time.sort()
if not use_cache:
if display_ob.name not in self.cached["timebeads_speed"]:
self.cached["timebeads_speed"][display_ob.name] = {}
for frame, sides in angles.items():
if sides["left"]:
perc = (sum(sides["left"]) / len(sides["left"])) / \
(math.pi / 2)
perc = max(0.4, min(1, perc * 5))
previous = kf_time[kf_time.index(frame) - 1]
bead_frame = frame - perc * ((frame - previous - 2) / 2)
if use_cache and bead_frame in \
self.cached["timebeads_speed"][display_ob.name]:
loc = self.cached["timebeads_speed"][display_ob.name]\
[bead_frame]
else:
loc = get_location(bead_frame, display_ob, offset_ob,
curves)
self.cached["timebeads_speed"][display_ob.name]\
[bead_frame] = loc
x, y = world_to_screen(context, loc)
timebeads[bead_frame] = [x, y]
click.append([bead_frame, "timebead", mathutils.\
Vector([x,y]), action_ob, child])
if sides["right"]:
perc = (sum(sides["right"]) / len(sides["right"])) / \
(math.pi / 2)
perc = max(0.4, min(1, perc * 5))
next = kf_time[kf_time.index(frame) + 1]
bead_frame = frame + perc * ((next - frame - 2) / 2)
if use_cache and bead_frame in \
self.cached["timebeads_speed"][display_ob.name]:
loc = self.cached["timebeads_speed"][display_ob.name]\
[bead_frame]
else:
loc = get_location(bead_frame, display_ob, offset_ob,
curves)
self.cached["timebeads_speed"][display_ob.name]\
[bead_frame] = loc
x, y = world_to_screen(context, loc)
timebeads[bead_frame] = [x, y]
click.append([bead_frame, "timebead", mathutils.\
Vector([x,y]), action_ob, child])
self.timebeads[display_ob.name] = timebeads
# add frame positions to click-list
if context.window_manager.motion_trail.frame_display:
path = self.paths[display_ob.name]
for x, y, colour, frame, action_ob, child in path:
click.append([frame, "frame", mathutils.Vector([x,y]),
action_ob, child])
self.click[display_ob.name] = click
if context.scene.frame_current != frame_old:
context.scene.frame_set(frame_old)
context.user_preferences.edit.use_global_undo = global_undo
# draw in 3d-view
def draw_callback(self, context):
# polling
if context.mode not in ['OBJECT', 'POSE'] or \
context.window_manager.motion_trail.enabled != 1:
return
# display limits
if context.window_manager.motion_trail.path_before != 0:
limit_min = context.scene.frame_current - \
context.window_manager.motion_trail.path_before
else:
limit_min = -1e6
if context.window_manager.motion_trail.path_after != 0:
limit_max = context.scene.frame_current + \
context.window_manager.motion_trail.path_after
else:
limit_max = 1e6
# draw motion path
bgl.glEnable(bgl.GL_BLEND)
bgl.glLineWidth(context.window_manager.motion_trail.path_width)
alpha = 1.0 - (context.window_manager.motion_trail.path_transparency / \
100.0)
if context.window_manager.motion_trail.path_style == 'simple':
bgl.glColor4f(0.0, 0.0, 0.0, alpha)
for objectname, path in self.paths.items():
bgl.glBegin(bgl.GL_LINE_STRIP)
for x, y, colour, frame, action_ob, child in path:
if frame < limit_min or frame > limit_max:
continue
bgl.glVertex2i(x, y)
bgl.glEnd()
else:
for objectname, path in self.paths.items():
for i, [x, y, colour, frame, action_ob, child] in enumerate(path):
if frame < limit_min or frame > limit_max:
continue
r, g, b = colour
if i != 0:
prev_path = path[i-1]
halfway = [(x + prev_path[0])/2, (y + prev_path[1])/2]
bgl.glColor4f(r, g, b, alpha)
bgl.glBegin(bgl.GL_LINE_STRIP)
bgl.glVertex2i(int(halfway[0]), int(halfway[1]))
bgl.glVertex2i(x, y)
bgl.glEnd()
if i != len(path) - 1:
next_path = path[i+1]
halfway = [(x + next_path[0])/2, (y + next_path[1])/2]
bgl.glColor4f(r, g, b, alpha)
bgl.glBegin(bgl.GL_LINE_STRIP)
bgl.glVertex2i(x, y)
bgl.glVertex2i(int(halfway[0]), int(halfway[1]))
bgl.glEnd()
# draw frames
if context.window_manager.motion_trail.frame_display:
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
bgl.glPointSize(1)
bgl.glBegin(bgl.GL_POINTS)
for objectname, path in self.paths.items():
for x, y, colour, frame, action_ob, child in path:
if frame < limit_min or frame > limit_max:
continue
if self.active_frame and objectname == self.active_frame[0] \
and abs(frame - self.active_frame[1]) < 1e-4:
bgl.glEnd()
bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
bgl.glPointSize(3)
bgl.glBegin(bgl.GL_POINTS)
bgl.glVertex2i(x,y)
bgl.glEnd()
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
bgl.glPointSize(1)
bgl.glBegin(bgl.GL_POINTS)
else:
bgl.glVertex2i(x,y)
bgl.glEnd()
# time beads are shown in speed and timing modes
if context.window_manager.motion_trail.mode in ['speed', 'timing']:
bgl.glColor4f(0.0, 1.0, 0.0, 1.0)
bgl.glPointSize(4)
bgl.glBegin(bgl.GL_POINTS)
for objectname, values in self.timebeads.items():
for frame, coords in values.items():
if frame < limit_min or frame > limit_max:
continue
if self.active_timebead and \
objectname == self.active_timebead[0] and \
abs(frame - self.active_timebead[1]) < 1e-4:
bgl.glEnd()
bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
bgl.glBegin(bgl.GL_POINTS)
bgl.glVertex2i(coords[0], coords[1])
bgl.glEnd()
bgl.glColor4f(0.0, 1.0, 0.0, 1.0)
bgl.glBegin(bgl.GL_POINTS)
else:
bgl.glVertex2i(coords[0], coords[1])
bgl.glEnd()
# handles are only shown in location mode
if context.window_manager.motion_trail.mode == 'location':
# draw handle-lines
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
bgl.glLineWidth(1)
bgl.glBegin(bgl.GL_LINES)
for objectname, values in self.handles.items():
for frame, sides in values.items():
if frame < limit_min or frame > limit_max:
continue
for side, coords in sides.items():
if self.active_handle and \
objectname == self.active_handle[0] and \
side == self.active_handle[2] and \
abs(frame - self.active_handle[1]) < 1e-4:
bgl.glEnd()
bgl.glColor4f(.75, 0.25, 0.0, 1.0)
bgl.glBegin(bgl.GL_LINES)
bgl.glVertex2i(self.keyframes[objectname][frame][0],
self.keyframes[objectname][frame][1])
bgl.glVertex2i(coords[0], coords[1])
bgl.glEnd()
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
bgl.glBegin(bgl.GL_LINES)
else:
bgl.glVertex2i(self.keyframes[objectname][frame][0],
self.keyframes[objectname][frame][1])
bgl.glVertex2i(coords[0], coords[1])
bgl.glEnd()
# draw handles
bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
bgl.glPointSize(4)
bgl.glBegin(bgl.GL_POINTS)
for objectname, values in self.handles.items():
for frame, sides in values.items():
if frame < limit_min or frame > limit_max:
continue
for side, coords in sides.items():
if self.active_handle and \
objectname == self.active_handle[0] and \
side == self.active_handle[2] and \
abs(frame - self.active_handle[1]) < 1e-4:
bgl.glEnd()
bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
bgl.glBegin(bgl.GL_POINTS)
bgl.glVertex2i(coords[0], coords[1])
bgl.glEnd()
bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
bgl.glBegin(bgl.GL_POINTS)
else:
bgl.glVertex2i(coords[0], coords[1])
bgl.glEnd()
# draw keyframes
bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
bgl.glPointSize(6)
bgl.glBegin(bgl.GL_POINTS)
for objectname, values in self.keyframes.items():
for frame, coords in values.items():
if frame < limit_min or frame > limit_max:
continue
if self.active_keyframe and \
objectname == self.active_keyframe[0] and \
abs(frame - self.active_keyframe[1]) < 1e-4:
bgl.glEnd()
bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
bgl.glBegin(bgl.GL_POINTS)
bgl.glVertex2i(coords[0], coords[1])
bgl.glEnd()
bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
bgl.glBegin(bgl.GL_POINTS)
else:
bgl.glVertex2i(coords[0], coords[1])
bgl.glEnd()
# draw keyframe-numbers
if context.window_manager.motion_trail.keyframe_numbers:
blf.size(0, 12, 72)
bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
for objectname, values in self.keyframes.items():
for frame, coords in values.items():
if frame < limit_min or frame > limit_max:
continue
blf.position(0, coords[0] + 3, coords[1] + 3, 0)
text = str(frame).split(".")
if len(text) == 1:
text = text[0]
elif len(text[1]) == 1 and text[1] == "0":
text = text[0]
else:
text = text[0] + "." + text[1][0]
if self.active_keyframe and \
objectname == self.active_keyframe[0] and \
abs(frame - self.active_keyframe[1]) < 1e-4:
bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
blf.draw(0, text)
bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
else:
blf.draw(0, text)
# restore opengl defaults
bgl.glLineWidth(1)
bgl.glDisable(bgl.GL_BLEND)
bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
bgl.glPointSize(1)
# change data based on mouse movement
def drag(context, event, drag_mouse_ori, active_keyframe, active_handle,
active_timebead, keyframes_ori, handles_ori, edit_bones):
# change 3d-location of keyframe
if context.window_manager.motion_trail.mode == 'location' and \
active_keyframe:
objectname, frame, frame_ori, action_ob, child = active_keyframe
if child:
mat = action_ob.matrix_world.copy().inverted() * \
edit_bones[child.name].copy().to_4x4()
else:
mat = 1
mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
drag_mouse_ori[1]) * mat
vector = screen_to_world(context, event.mouse_region_x,
event.mouse_region_y) * mat
d = vector - mouse_ori_world
loc_ori_ws = keyframes_ori[objectname][frame][1]
loc_ori_bs = loc_ori_ws * mat
new_loc = loc_ori_bs + d
curves = get_curves(action_ob, child)
for i, curve in enumerate(curves):
for kf in curve.keyframe_points:
if kf.co[0] == frame:
kf.co[1] = new_loc[i]
kf.handle_left[1] = handles_ori[objectname][frame]\
["left"][i][1] + d[i]
kf.handle_right[1] = handles_ori[objectname][frame]\
["right"][i][1] + d[i]
break
# change 3d-location of handle
elif context.window_manager.motion_trail.mode == 'location' and \
active_handle:
objectname, frame, side, action_ob, child = active_handle
if child:
mat = action_ob.matrix_world.copy().inverted() * \
edit_bones[child.name].copy().to_4x4()
else:
mat = 1
mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
drag_mouse_ori[1]) * mat
vector = screen_to_world(context, event.mouse_region_x,
event.mouse_region_y) * mat
d = vector - mouse_ori_world
curves = get_curves(action_ob, child)
for i, curve in enumerate(curves):
for kf in curve.keyframe_points:
if kf.co[0] == frame:
if side == "left":
# change handle type, if necessary
if kf.handle_left_type in ['AUTO', 'ANIM_CLAMPED']:
kf.handle_left_type = 'ALIGNED'
elif kf.handle_left_type == 'VECTOR':
kf.handle_left_type = 'FREE'
# change handle position(s)
kf.handle_left[1] = handles_ori[objectname][frame]\
["left"][i][1] + d[i]
if kf.handle_left_type in ['ALIGNED', 'ANIM_CLAMPED',
'AUTO']:
dif = (abs(handles_ori[objectname][frame]["right"]\
[i][0] - kf.co[0]) / abs(kf.handle_left[0] - \
kf.co[0])) * d[i]
kf.handle_right[1] = handles_ori[objectname]\
[frame]["right"][i][1] - dif
elif side == "right":
# change handle type, if necessary
if kf.handle_right_type in ['AUTO', 'ANIM_CLAMPED']:
kf.handle_left_type = 'ALIGNED'
kf.handle_right_type = 'ALIGNED'
elif kf.handle_right_type == 'VECTOR':
kf.handle_left_type = 'FREE'
kf.handle_right_type = 'FREE'
# change handle position(s)
kf.handle_right[1] = handles_ori[objectname][frame]\
["right"][i][1] + d[i]
if kf.handle_right_type in ['ALIGNED', 'ANIM_CLAMPED',
'AUTO']:
dif = (abs(handles_ori[objectname][frame]["left"]\
[i][0] - kf.co[0]) / abs(kf.handle_right[0] - \
kf.co[0])) * d[i]
kf.handle_left[1] = handles_ori[objectname]\
[frame]["left"][i][1] - dif
break
# change position of all keyframes on timeline
elif context.window_manager.motion_trail.mode == 'timing' and \
active_timebead:
objectname, frame, frame_ori, action_ob, child = active_timebead
curves = get_curves(action_ob, child)
ranges = [val for c in curves for val in c.range()]
ranges.sort()
range_min = round(ranges[0])
range_max = round(ranges[-1])
range = range_max - range_min
dx_screen = -(mathutils.Vector([event.mouse_region_x,
event.mouse_region_y]) - drag_mouse_ori)[0]
dx_screen = dx_screen / context.region.width * range
new_frame = frame + dx_screen
shift_low = max(1e-4, (new_frame - range_min) / (frame - range_min))
shift_high = max(1e-4, (range_max - new_frame) / (range_max - frame))
new_mapping = {}
for i, curve in enumerate(curves):
for j, kf in enumerate(curve.keyframe_points):
frame_map = kf.co[0]
if frame_map < range_min + 1e-4 or \
frame_map > range_max - 1e-4:
continue
frame_ori = False
for f in keyframes_ori[objectname]:
if abs(f - frame_map) < 1e-4:
frame_ori = keyframes_ori[objectname][f][0]
value_ori = keyframes_ori[objectname][f]
break
if not frame_ori:
continue
if frame_ori <= frame:
frame_new = (frame_ori - range_min) * shift_low + \
range_min
else:
frame_new = range_max - (range_max - frame_ori) * \
shift_high
frame_new = max(range_min + j, min(frame_new, range_max - \
(len(curve.keyframe_points)-j)+1))
d_frame = frame_new - frame_ori
if frame_new not in new_mapping:
new_mapping[frame_new] = value_ori
kf.co[0] = frame_new
kf.handle_left[0] = handles_ori[objectname][frame_ori]\
["left"][i][0] + d_frame
kf.handle_right[0] = handles_ori[objectname][frame_ori]\
["right"][i][0] + d_frame
del keyframes_ori[objectname]
keyframes_ori[objectname] = {}
for new_frame, value in new_mapping.items():
keyframes_ori[objectname][new_frame] = value
# change position of active keyframe on the timeline
elif context.window_manager.motion_trail.mode == 'timing' and \
active_keyframe:
objectname, frame, frame_ori, action_ob, child = active_keyframe
if child:
mat = action_ob.matrix_world.copy().inverted() * \
edit_bones[child.name].copy().to_4x4()
else:
mat = action_ob.matrix_world.copy().inverted()
mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
drag_mouse_ori[1]) * mat
vector = screen_to_world(context, event.mouse_region_x,
event.mouse_region_y) * mat
d = vector - mouse_ori_world
locs_ori = [[f_ori, coords] for f_mapped, [f_ori, coords] in \
keyframes_ori[objectname].items()]
locs_ori.sort()
direction = 1
range = False
for i, [f_ori, coords] in enumerate(locs_ori):
if abs(frame_ori - f_ori) < 1e-4:
if i == 0:
# first keyframe, nothing before it
direction = -1
range = [f_ori, locs_ori[i+1][0]]
elif i == len(locs_ori) - 1:
# last keyframe, nothing after it
range = [locs_ori[i-1][0], f_ori]
else:
current = mathutils.Vector(coords)
next = mathutils.Vector(locs_ori[i+1][1])
previous = mathutils.Vector(locs_ori[i-1][1])
angle_to_next = d.angle(next - current, 0)
angle_to_previous = d.angle(previous-current, 0)
if angle_to_previous < angle_to_next:
# mouse movement is in direction of previous keyframe
direction = -1
range = [locs_ori[i-1][0], locs_ori[i+1][0]]
break
direction *= -1 # feels more natural in 3d-view
if not range:
# keyframe not found, is impossible, but better safe than sorry
return(active_keyframe, active_timebead, keyframes_ori)
# calculate strength of movement
d_screen = mathutils.Vector([event.mouse_region_x,
event.mouse_region_y]) - drag_mouse_ori
if d_screen.length != 0:
d_screen = d_screen.length / (abs(d_screen[0])/d_screen.length*\
context.region.width + abs(d_screen[1])/d_screen.length*\
context.region.height)
d_screen *= direction # d_screen value ranges from -1.0 to 1.0
else:
d_screen = 0.0
new_frame = d_screen * (range[1] - range[0]) + frame_ori
max_frame = range[1]
if max_frame == frame_ori:
max_frame += 1
min_frame = range[0]
if min_frame == frame_ori:
min_frame -= 1
new_frame = min(max_frame - 1, max(min_frame + 1, new_frame))
d_frame = new_frame - frame_ori
curves = get_curves(action_ob, child)
for i, curve in enumerate(curves):
for kf in curve.keyframe_points:
if abs(kf.co[0] - frame) < 1e-4:
kf.co[0] = new_frame
kf.handle_left[0] = handles_ori[objectname][frame_ori]\
["left"][i][0] + d_frame
kf.handle_right[0] = handles_ori[objectname][frame_ori]\
["right"][i][0] + d_frame
break
active_keyframe = [objectname, new_frame, frame_ori, action_ob, child]
# change position of active timebead on the timeline, thus altering speed
elif context.window_manager.motion_trail.mode == 'speed' and \
active_timebead:
objectname, frame, frame_ori, action_ob, child = active_timebead
if child:
mat = action_ob.matrix_world.copy().inverted() * \
edit_bones[child.name].copy().to_4x4()
else:
mat = 1
mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
drag_mouse_ori[1]) * mat
vector = screen_to_world(context, event.mouse_region_x,
event.mouse_region_y) * mat
d = vector - mouse_ori_world
# determine direction (to next or previous keyframe)
curves = get_curves(action_ob, child)
fcx, fcy, fcz = curves
locx = fcx.evaluate(frame_ori)
locy = fcy.evaluate(frame_ori)
locz = fcz.evaluate(frame_ori)
loc_ori = mathutils.Vector([locx, locy, locz]) # bonespace
keyframes = [kf for kf in keyframes_ori[objectname]]
keyframes.append(frame_ori)
keyframes.sort()
frame_index = keyframes.index(frame_ori)
kf_prev = keyframes[frame_index - 1]
kf_next = keyframes[frame_index + 1]
vec_prev = (mathutils.Vector(keyframes_ori[objectname][kf_prev][1]) \
* mat - loc_ori).normalized()
vec_next = (mathutils.Vector(keyframes_ori[objectname][kf_next][1]) \
* mat - loc_ori).normalized()
d_normal = d.copy().normalized()
dist_to_next = (d_normal - vec_next).length
dist_to_prev = (d_normal - vec_prev).length
if dist_to_prev < dist_to_next:
direction = 1
else:
direction = -1
if (kf_next - frame_ori) < (frame_ori - kf_prev):
kf_bead = kf_next
side = "left"
else:
kf_bead = kf_prev
side = "right"
d_frame = d.length * direction * 2 # *2 to make it more sensitive
angles = []
for i, curve in enumerate(curves):
for kf in curve.keyframe_points:
if abs(kf.co[0] - kf_bead) < 1e-4:
if side == "left":
# left side
kf.handle_left[0] = min(handles_ori[objectname]\
[kf_bead]["left"][i][0] + d_frame, kf_bead - 1)
angle = mathutils.Vector([-1, 0]).angle(mathutils.\
Vector(kf.handle_left) - mathutils.Vector(kf.co),
0)
if angle != 0:
angles.append(angle)
else:
# right side
kf.handle_right[0] = max(handles_ori[objectname]\
[kf_bead]["right"][i][0] + d_frame, kf_bead + 1)
angle = mathutils.Vector([1, 0]).angle(mathutils.\
Vector(kf.handle_right) - mathutils.Vector(kf.co),
0)
if angle != 0:
angles.append(angle)
break
# update frame of active_timebead
perc = (sum(angles) / len(angles)) / (math.pi / 2)
perc = max(0.4, min(1, perc * 5))
if side == "left":
bead_frame = kf_bead - perc * ((kf_bead - kf_prev - 2) / 2)
else:
bead_frame = kf_bead + perc * ((kf_next - kf_bead - 2) / 2)
active_timebead = [objectname, bead_frame, frame_ori, action_ob, child]
return(active_keyframe, active_timebead, keyframes_ori)
# revert changes made by dragging
def cancel_drag(context, active_keyframe, active_handle, active_timebead,
keyframes_ori, handles_ori, edit_bones):
# revert change in 3d-location of active keyframe and its handles
if context.window_manager.motion_trail.mode == 'location' and \
active_keyframe:
objectname, frame, frame_ori, active_ob, child = active_keyframe
curves = get_curves(active_ob, child)
loc_ori = keyframes_ori[objectname][frame][1]
if child:
loc_ori = loc_ori * edit_bones[child.name] * \
active_ob.matrix_world.copy().inverted()
for i, curve in enumerate(curves):
for kf in curve.keyframe_points:
if kf.co[0] == frame:
kf.co[1] = loc_ori[i]
kf.handle_left[1] = handles_ori[objectname][frame]\
["left"][i][1]
kf.handle_right[1] = handles_ori[objectname][frame]\
["right"][i][1]
break
# revert change in 3d-location of active handle
elif context.window_manager.motion_trail.mode == 'location' and \
active_handle:
objectname, frame, side, active_ob, child = active_handle
curves = get_curves(active_ob, child)
for i, curve in enumerate(curves):
for kf in curve.keyframe_points:
if kf.co[0] == frame:
kf.handle_left[1] = handles_ori[objectname][frame]\
["left"][i][1]
kf.handle_right[1] = handles_ori[objectname][frame]\
["right"][i][1]
break
# revert position of all keyframes and handles on timeline
elif context.window_manager.motion_trail.mode == 'timing' and \
active_timebead:
objectname, frame, frame_ori, active_ob, child = active_timebead
curves = get_curves(active_ob, child)
for i, curve in enumerate(curves):
for kf in curve.keyframe_points:
for kf_ori, [frame_ori, loc] in keyframes_ori[objectname].\
items():
if abs(kf.co[0] - kf_ori) < 1e-4:
kf.co[0] = frame_ori
kf.handle_left[0] = handles_ori[objectname]\
[frame_ori]["left"][i][0]
kf.handle_right[0] = handles_ori[objectname]\
[frame_ori]["right"][i][0]
break
# revert position of active keyframe and its handles on the timeline
elif context.window_manager.motion_trail.mode == 'timing' and \
active_keyframe:
objectname, frame, frame_ori, active_ob, child = active_keyframe
curves = get_curves(active_ob, child)
for i, curve in enumerate(curves):
for kf in curve.keyframe_points:
if abs(kf.co[0] - frame) < 1e-4:
kf.co[0] = keyframes_ori[objectname][frame_ori][0]
kf.handle_left[0] = handles_ori[objectname][frame_ori]\
["left"][i][0]
kf.handle_right[0] = handles_ori[objectname][frame_ori]\
["right"][i][0]
break
active_keyframe = [objectname, frame_ori, frame_ori, active_ob, child]
# revert position of handles on the timeline
elif context.window_manager.motion_trail.mode == 'speed' and \
active_timebead:
objectname, frame, frame_ori, active_ob, child = active_timebead
curves = get_curves(active_ob, child)
keyframes = [kf for kf in keyframes_ori[objectname]]
keyframes.append(frame_ori)
keyframes.sort()
frame_index = keyframes.index(frame_ori)
kf_prev = keyframes[frame_index - 1]
kf_next = keyframes[frame_index + 1]
if (kf_next - frame_ori) < (frame_ori - kf_prev):
kf_frame = kf_next
else:
kf_frame = kf_prev
for i, curve in enumerate(curves):
for kf in curve.keyframe_points:
if kf.co[0] == kf_frame:
kf.handle_left[0] = handles_ori[objectname][kf_frame]\
["left"][i][0]
kf.handle_right[0] = handles_ori[objectname][kf_frame]\
["right"][i][0]
break
active_timebead = [objectname, frame_ori, frame_ori, active_ob, child]
return(active_keyframe, active_timebead)
# return the handle type of the active selection
def get_handle_type(active_keyframe, active_handle):
if active_keyframe:
objectname, frame, side, action_ob, child = active_keyframe
side = "both"
elif active_handle:
objectname, frame, side, action_ob, child = active_handle
else:
# no active handle(s)
return(False)
# properties used when changing handle type
bpy.context.window_manager.motion_trail.handle_type_frame = frame
bpy.context.window_manager.motion_trail.handle_type_side = side
bpy.context.window_manager.motion_trail.handle_type_action_ob = \
action_ob.name
if child:
bpy.context.window_manager.motion_trail.handle_type_child = child.name
else:
bpy.context.window_manager.motion_trail.handle_type_child = ""
curves = get_curves(action_ob, child=child)
for c in curves:
for kf in c.keyframe_points:
if kf.co[0] == frame:
if side in ["left", "both"]:
return(kf.handle_left_type)
else:
return(kf.handle_right_type)
return("AUTO")
# turn the given frame into a keyframe
def insert_keyframe(self, context, frame):
objectname, frame, frame, action_ob, child = frame
curves = get_curves(action_ob, child)
for c in curves:
y = c.evaluate(frame)
if c.keyframe_points:
c.keyframe_points.insert(frame, y)
bpy.context.window_manager.motion_trail.force_update = True
calc_callback(self, context)
# change the handle type of the active selection
def set_handle_type(self, context):
if not context.window_manager.motion_trail.handle_type_enabled:
return
if context.window_manager.motion_trail.handle_type_old == \
context.window_manager.motion_trail.handle_type:
# function called because of selection change, not change in type
return
context.window_manager.motion_trail.handle_type_old = \
context.window_manager.motion_trail.handle_type
frame = bpy.context.window_manager.motion_trail.handle_type_frame
side = bpy.context.window_manager.motion_trail.handle_type_side
action_ob = bpy.context.window_manager.motion_trail.handle_type_action_ob
action_ob = bpy.data.objects[action_ob]
child = bpy.context.window_manager.motion_trail.handle_type_child
if child:
child = action_ob.pose.bones[child]
new_type = context.window_manager.motion_trail.handle_type
curves = get_curves(action_ob, child=child)
for c in curves:
for kf in c.keyframe_points:
if kf.co[0] == frame:
# align if necessary
if side in ["right", "both"] and new_type in \
["AUTO", "ALIGNED"]:
# change right handle
normal = (kf.co - kf.handle_left).normalized()
size = (kf.handle_right[0] - kf.co[0]) / normal[0]
normal = normal*size + kf.co
kf.handle_right[1] = normal[1]
elif side == "left" and new_type in ["AUTO", "ALIGNED"]:
# change left handle
normal = (kf.co - kf.handle_right).normalized()
size = (kf.handle_left[0] - kf.co[0]) / normal[0]
normal = normal*size + kf.co
kf.handle_left[1] = normal[1]
# change type
if side in ["left", "both"]:
kf.handle_left_type = new_type
if side in ["right", "both"]:
kf.handle_right_type = new_type
context.window_manager.motion_trail.force_update = True
class MotionTrailOperator(bpy.types.Operator):
'''Edit motion trails in 3d-view'''
bl_idname = "view3d.motion_trail"
bl_label = "Motion Trail"
_handle1 = None
_handle2 = None
def modal(self, context, event):
if context.window_manager.motion_trail.enabled == -1:
context.window_manager.motion_trail.enabled = 0
try:
context.region.callback_remove(self._handle1)
except:
pass
try:
context.region.callback_remove(self._handle2)
except:
pass
context.area.tag_redraw()
return {'FINISHED'}
if not context.area:
return {'PASS_THROUGH'}
if not context.region or event.type == 'NONE':
context.area.tag_redraw()
return {'PASS_THROUGH'}
select = context.user_preferences.inputs.select_mouse
if not context.active_object or not context.active_object.mode in \
['OBJECT', 'POSE']:
if self.drag:
self.drag = False
self.lock = True
context.window_manager.motion_trail.force_update = True
# default hotkeys should still work
if event.type == self.transform_key and event.value == 'PRESS':
if bpy.ops.transform.translate.poll():
bpy.ops.transform.translate('INVOKE_DEFAULT')
elif event.type == select + 'MOUSE' and event.value == 'PRESS' \
and not self.drag and not event.shift and not event.alt \
and not event.ctrl:
if bpy.ops.view3d.select.poll():
bpy.ops.view3d.select('INVOKE_DEFAULT')
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS' and not\
event.alt and not event.ctrl and not event.shift:
if eval("bpy.ops."+self.left_action+".poll()"):
eval("bpy.ops."+self.left_action+"('INVOKE_DEFAULT')")
return {'PASS_THROUGH'}
# check if event was generated within 3d-window, dragging is exception
if not self.drag:
if not (0 < event.mouse_region_x < context.region.width) or \
not (0 < event.mouse_region_y < context.region.height):
return {'PASS_THROUGH'}
if event.type == self.transform_key and event.value == 'PRESS' and \
(self.active_keyframe or self.active_handle or self.active_timebead \
or self.active_frame):
# override default translate()
if not self.drag:
# start drag
if self.active_frame:
insert_keyframe(self, context, self.active_frame)
self.active_keyframe = self.active_frame
self.active_frame = False
self.keyframes_ori, self.handles_ori = \
get_original_animation_data(context, self.keyframes)
self.drag_mouse_ori = mathutils.Vector([event.mouse_region_x,
event.mouse_region_y])
self.drag = True
self.lock = False
else:
# stop drag
self.drag = False
self.lock = True
context.window_manager.motion_trail.force_update = True
elif event.type == self.transform_key and event.value == 'PRESS':
# call default translate()
if bpy.ops.transform.translate.poll():
bpy.ops.transform.translate('INVOKE_DEFAULT')
elif (event.type == 'ESC' and self.drag and event.value == 'PRESS') \
or (event.type == 'RIGHTMOUSE' and self.drag and event.value == \
'PRESS'):
# cancel drag
self.drag = False
self.lock = True
context.window_manager.motion_trail.force_update = True
self.active_keyframe, self.active_timebead = cancel_drag(context,
self.active_keyframe, self.active_handle,
self.active_timebead, self.keyframes_ori, self.handles_ori,
self.edit_bones)
elif event.type == 'MOUSEMOVE' and self.drag:
# drag
self.active_keyframe, self.active_timebead, self.keyframes_ori = \
drag(context, event, self.drag_mouse_ori,
self.active_keyframe, self.active_handle,
self.active_timebead, self.keyframes_ori, self.handles_ori,
self.edit_bones)
elif event.type == select + 'MOUSE' and event.value == 'PRESS' and \
not self.drag and not event.shift and not event.alt and not \
event.ctrl:
# select
treshold = 10
clicked = mathutils.Vector([event.mouse_region_x,
event.mouse_region_y])
self.active_keyframe = False
self.active_handle = False
self.active_timebead = False
self.active_frame = False
context.window_manager.motion_trail.force_update = True
context.window_manager.motion_trail.handle_type_enabled = True
found = False
if context.window_manager.motion_trail.path_before == 0:
frame_min = context.scene.frame_start
else:
frame_min = max(context.scene.frame_start,
context.scene.frame_current - \
context.window_manager.motion_trail.path_before)
if context.window_manager.motion_trail.path_after == 0:
frame_max = context.scene.frame_end
else:
frame_max = min(context.scene.frame_end,
context.scene.frame_current + \
context.window_manager.motion_trail.path_after)
for objectname, values in self.click.items():
if found:
break
for frame, type, coord, action_ob, child in values:
if frame < frame_min or frame > frame_max:
continue
if (coord - clicked).length <= treshold:
found = True
if type == "keyframe":
self.active_keyframe = [objectname, frame, frame,
action_ob, child]
elif type == "handle_left":
self.active_handle = [objectname, frame, "left",
action_ob, child]
elif type == "handle_right":
self.active_handle = [objectname, frame, "right",
action_ob, child]
elif type == "timebead":
self.active_timebead = [objectname, frame, frame,
action_ob, child]
elif type == "frame":
self.active_frame = [objectname, frame, frame,
action_ob, child]
break
if not found:
context.window_manager.motion_trail.handle_type_enabled = False
# no motion trail selections, so pass on to normal select()
if bpy.ops.view3d.select.poll():
bpy.ops.view3d.select('INVOKE_DEFAULT')
else:
handle_type = get_handle_type(self.active_keyframe,
self.active_handle)
if handle_type:
context.window_manager.motion_trail.handle_type_old = \
handle_type
context.window_manager.motion_trail.handle_type = \
handle_type
else:
context.window_manager.motion_trail.handle_type_enabled = \
False
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS' and \
self.drag:
# stop drag
self.drag = False
self.lock = True
context.window_manager.motion_trail.force_update = True
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS' and not\
event.alt and not event.ctrl and not event.shift:
if eval("bpy.ops."+self.left_action+".poll()"):
eval("bpy.ops."+self.left_action+"('INVOKE_DEFAULT')")
if context.area: # not available if other window-type is fullscreen
context.area.tag_redraw()
return {'PASS_THROUGH'}
def invoke(self, context, event):
if context.area.type == 'VIEW_3D':
# get clashing keymap items
select = context.user_preferences.inputs.select_mouse
kms = [bpy.context.window_manager.keyconfigs.active.\
keymaps['3D View'], bpy.context.window_manager.keyconfigs.\
active.keymaps['Object Mode']]
kmis = []
self.left_action = None
self.right_action = None
for km in kms:
for kmi in km.keymap_items:
if kmi.idname == "transform.translate" and \
kmi.map_type == 'KEYBOARD' and not \
kmi.properties.texture_space:
kmis.append(kmi)
self.transform_key = kmi.type
elif (kmi.type == 'ACTIONMOUSE' and select == 'RIGHT') \
and not kmi.alt and not kmi.any and not kmi.ctrl \
and not kmi.shift:
kmis.append(kmi)
self.left_action = kmi.idname
elif kmi.type == 'SELECTMOUSE' and not kmi.alt and not \
kmi.any and not kmi.ctrl and not kmi.shift:
kmis.append(kmi)
if select == 'RIGHT':
self.right_action = kmi.idname
else:
self.left_action = kmi.idname
elif kmi.type == 'LEFTMOUSE' and not kmi.alt and not \
kmi.any and not kmi.ctrl and not kmi.shift:
kmis.append(kmi)
self.left_action = kmi.idname
if context.window_manager.motion_trail.enabled == 0:
# enable
context.window_manager.motion_trail.enabled = 1
self.active_keyframe = False
self.active_handle = False
self.active_timebead = False
self.active_frame = False
self.click = {}
self.drag = False
self.lock = True
self.perspective = context.region_data.perspective_matrix
self.displayed = []
context.window_manager.motion_trail.force_update = True
context.window_manager.motion_trail.handle_type_enabled = False
self.cached = {"path":{}, "keyframes":{},
"timebeads_timing":{}, "timebeads_speed":{}}
for kmi in kmis:
kmi.active = False
context.window_manager.modal_handler_add(self)
self._handle1 = context.region.callback_add(calc_callback,
(self, context), 'POST_VIEW')
self._handle2 = context.region.callback_add(draw_callback,
(self, context), 'POST_PIXEL')
if context.area:
context.area.tag_redraw()
else:
# disable
context.window_manager.motion_trail.enabled = -1
for kmi in kmis:
kmi.active = True
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
class MotionTrailPanel(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_label = "Motion Trail"
@classmethod
def poll(cls, context):
if not context.active_object:
return(False)
return(context.active_object.mode in ['OBJECT', 'POSE'])
def draw(self, context):
col = self.layout.column()
if context.window_manager.motion_trail.enabled != 1:
col.operator("view3d.motion_trail", text="Enable motion trail")
else:
col.operator("view3d.motion_trail", text="Disable motion trail")
box = self.layout.box()
box.prop(context.window_manager.motion_trail, "mode")
#box.prop(context.window_manager.motion_trail, "calculate")
if context.window_manager.motion_trail.mode in ['timing']:
box.prop(context.window_manager.motion_trail, "timebeads")
box = self.layout.box()
col = box.column()
row = col.row()
if context.window_manager.motion_trail.path_display:
row.prop(context.window_manager.motion_trail, "path_display",
icon="DOWNARROW_HLT", text="", emboss=False)
else:
row.prop(context.window_manager.motion_trail, "path_display",
icon="RIGHTARROW", text="", emboss=False)
row.label("Path options")
if context.window_manager.motion_trail.path_display:
col.prop(context.window_manager.motion_trail, "path_style",
text="Style")
grouped = col.column(align=True)
grouped.prop(context.window_manager.motion_trail, "path_width",
text="Width")
grouped.prop(context.window_manager.motion_trail,
"path_transparency", text="Transparency")
grouped.prop(context.window_manager.motion_trail,
"path_resolution")
row = grouped.row(align=True)
row.prop(context.window_manager.motion_trail, "path_before")
row.prop(context.window_manager.motion_trail, "path_after")
col = col.column(align=True)
col.prop(context.window_manager.motion_trail, "keyframe_numbers")
col.prop(context.window_manager.motion_trail, "frame_display")
if context.window_manager.motion_trail.mode in ['location']:
box = self.layout.box()
col = box.column(align=True)
col.prop(context.window_manager.motion_trail, "handle_display",
text="Handles")
if context.window_manager.motion_trail.handle_display:
row = col.row()
row.enabled = context.window_manager.motion_trail.\
handle_type_enabled
row.prop(context.window_manager.motion_trail, "handle_type")
class MotionTrailProps(bpy.types.PropertyGroup):
def internal_update(self, context):
context.window_manager.motion_trail.force_update = True
if context.area:
context.area.tag_redraw()
# internal use
enabled = bpy.props.IntProperty(default=0)
force_update = bpy.props.BoolProperty(name="internal use",
description="Force calc_callback to fully execute",
default=False)
handle_type_enabled = bpy.props.BoolProperty(default=False)
handle_type_frame = bpy.props.FloatProperty()
handle_type_side = bpy.props.StringProperty()
handle_type_action_ob = bpy.props.StringProperty()
handle_type_child = bpy.props.StringProperty()
handle_type_old = bpy.props.EnumProperty(items=(("AUTO", "", ""),
("VECTOR", "", ""), ("ALIGNED", "", ""), ("FREE", "", "")),
default='AUTO',)
# visible in user interface
calculate = bpy.props.EnumProperty(name="Calculate",
items=(("fast", "Fast", "Recommended setting, change if the "\
"motion path is positioned incorrectly"),
("full", "Full", "Takes parenting and modifiers into account, "\
"but can be very slow on complicated scenes")),
description="Calculation method for determining locations",
default='full',
update=internal_update)
frame_display = bpy.props.BoolProperty(name="Frames",
description="Display frames, \n test",
default=True,
update=internal_update)
handle_display = bpy.props.BoolProperty(name="Display",
description="Display handles",
default=True,
update=internal_update)
handle_type = bpy.props.EnumProperty(name="Type",
items=(("AUTO", "Automatic", ""),
("VECTOR", "Vector", ""),
("ALIGNED", "Aligned", ""),
("FREE", "Free", "")),
description="Set handle type for the selected handle",
default='AUTO',
update=set_handle_type)
keyframe_numbers = bpy.props.BoolProperty(name="Keyframe numbers",
description="Display keyframe numbers",
default=False,
update=internal_update)
mode = bpy.props.EnumProperty(name="Mode",
items=(("location", "Location", "Change path that is followed"),
("speed", "Speed", "Change speed between keyframes"),
("timing", "Timing", "Change position of keyframes on timeline")),
description="Enable editing of certain properties in the 3d-view",
default='location',
update=internal_update)
path_after = bpy.props.IntProperty(name="After",
description="Number of frames to show after the current frame, "\
"0 = display all",
default=50,
min=0,
update=internal_update)
path_before = bpy.props.IntProperty(name="Before",
description="Number of frames to show before the current frame, "\
"0 = display all",
default=50,
min=0,
update=internal_update)
path_display = bpy.props.BoolProperty(name="Path options",
description="Display path options",
default=True)
path_resolution = bpy.props.IntProperty(name="Resolution",
description="10 is smoothest, but could be "\
"slow when adjusting keyframes, handles or timebeads",
default=10,
min=1,
max=10,
update=internal_update)
path_style = bpy.props.EnumProperty(name="Path style",
items=(("acceleration", "Acceleration", "Gradient based on relative "\
"acceleration"),
("simple", "Simple", "Black line"),
("speed", "Speed", "Gradient based on relative speed")),
description="Information conveyed by path colour",
default='simple',
update=internal_update)
path_transparency = bpy.props.IntProperty(name="Path transparency",
description="Determines visibility of path",
default=0,
min=0,
max=100,
subtype='PERCENTAGE',
update=internal_update)
path_width = bpy.props.IntProperty(name="Path width",
description="Width in pixels",
default=1,
min=1,
soft_max=5,
update=internal_update)
timebeads = bpy.props.IntProperty(name="Time beads",
description="Number of time beads to display per segment",
default=5,
min=1,
soft_max = 10,
update=internal_update)
classes = [MotionTrailProps,
MotionTrailOperator,
MotionTrailPanel]
def register():
for c in classes:
bpy.utils.register_class(c)
bpy.types.WindowManager.motion_trail = bpy.props.PointerProperty(\
type=MotionTrailProps)
def unregister():
for c in classes:
bpy.utils.unregister_class(c)
del bpy.types.WindowManager.motion_trail
if __name__ == "__main__":
register()