diff --git a/animation_motion_trail.py b/animation_motion_trail.py new file mode 100644 index 0000000000000000000000000000000000000000..978ae77b5a3c5437f89feec1776294f8ebb0fc84 --- /dev/null +++ b/animation_motion_trail.py @@ -0,0 +1,1736 @@ +# ##### 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), + 'api': 42181, + '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()