# ##### 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, 3), "blender": (2, 80, 0), "location": "View3D > Toolbar > Motion Trail tab", "warning": "Needs bgl draw update", "description": "Display and edit motion trails in the 3D View", "doc_url": "https://wiki.blender.org/index.php/Extensions:2.6/Py/" "Scripts/Animation/Motion_Trail", "tracker_url": "https://developer.blender.org/maniphest/task/edit/form/2/", "category": "Animation", } import bgl import blf import bpy from bpy_extras import view3d_utils import math import mathutils from bpy.props import ( BoolProperty, EnumProperty, FloatProperty, IntProperty, StringProperty, PointerProperty, ) # 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 is None: fcx = fake_fcurve(object, 0) if fcy is None: fcy = fake_fcurve(object, 1) if fcz is 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, color 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 try: global_undo = context.preferences.edit.use_global_undo context.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 color 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"] is not 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, color, 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.preferences.edit.use_global_undo = global_undo except: # restore global undo in case of failure (see T52524) context.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 not context.window_manager.motion_trail.enabled): 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, color, 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, color, frame, action_ob, child] in enumerate(path): if frame < limit_min or frame > limit_max: continue r, g, b = color 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, color, 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', 'AUTO_CLAMPED', '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', 'AUTO_CLAMPED'): 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', 'AUTO_CLAMPED', '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', 'AUTO_CLAMPED'): 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", "AUTO_CLAMPED", "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", "AUTO_CLAMPED", "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): bl_idname = "view3d.motion_trail" bl_label = "Motion Trail" bl_description = "Edit motion trails in 3d-view" _handle_calc = None _handle_draw = None @staticmethod def handle_add(self, context): MotionTrailOperator._handle_calc = bpy.types.SpaceView3D.draw_handler_add( calc_callback, (self, context), 'WINDOW', 'POST_VIEW') MotionTrailOperator._handle_draw = bpy.types.SpaceView3D.draw_handler_add( draw_callback, (self, context), 'WINDOW', 'POST_PIXEL') @staticmethod def handle_remove(): if MotionTrailOperator._handle_calc is not None: bpy.types.SpaceView3D.draw_handler_remove(MotionTrailOperator._handle_calc, 'WINDOW') if MotionTrailOperator._handle_draw is not None: bpy.types.SpaceView3D.draw_handler_remove(MotionTrailOperator._handle_draw, 'WINDOW') MotionTrailOperator._handle_calc = None MotionTrailOperator._handle_draw = None def modal(self, context, event): # XXX Required, or custom transform.translate will break! # XXX If one disables and re-enables motion trail, modal op will still be running, # XXX default translate op will unintentionally get called, followed by custom translate. if not context.window_manager.motion_trail.enabled: MotionTrailOperator.handle_remove() context.area.tag_redraw() return {'FINISHED'} if not context.area or not context.region or event.type == 'NONE': context.area.tag_redraw() return {'PASS_THROUGH'} wm = context.window_manager keyconfig = wm.keyconfigs.active select = getattr(keyconfig.preferences, "select_mouse", "LEFT") if (not context.active_object or context.active_object.mode not 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': self.report({'WARNING'}, "View3D not found, cannot run operator") return {'CANCELLED'} # get clashing keymap items wm = context.window_manager keyconfig = wm.keyconfigs.active select = getattr(keyconfig.preferences, "select_mouse", "LEFT") 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 not context.window_manager.motion_trail.enabled: # enable 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 MotionTrailOperator.handle_add(self, context) context.window_manager.motion_trail.enabled = True if context.area: context.area.tag_redraw() context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} else: # disable for kmi in kmis: kmi.active = True MotionTrailOperator.handle_remove() context.window_manager.motion_trail.enabled = False if context.area: context.area.tag_redraw() return {'FINISHED'} class MotionTrailPanel(bpy.types.Panel): bl_idname = "VIEW3D_PT_motion_trail" bl_category = "Animation" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_label = "Motion Trail" bl_options = {'DEFAULT_CLOSED'} @classmethod def poll(cls, context): if context.active_object is None: return False return context.active_object.mode in ('OBJECT', 'POSE') def draw(self, context): col = self.layout.column() if not context.window_manager.motion_trail.enabled: 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 == '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(text="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 == '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: BoolProperty(default=False) force_update: BoolProperty(name="internal use", description="Force calc_callback to fully execute", default=False) handle_type_enabled: BoolProperty(default=False) handle_type_frame: FloatProperty() handle_type_side: StringProperty() handle_type_action_ob: StringProperty() handle_type_child: StringProperty() handle_type_old: EnumProperty( items=( ("AUTO", "", ""), ("AUTO_CLAMPED", "", ""), ("VECTOR", "", ""), ("ALIGNED", "", ""), ("FREE", "", "")), default='AUTO' ) # visible in user interface calculate: 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: BoolProperty(name="Frames", description="Display frames, \n test", default=True, update=internal_update ) handle_display: BoolProperty(name="Display", description="Display handles", default=True, update=internal_update ) handle_type: EnumProperty(name="Type", items=( ("AUTO", "Automatic", ""), ("AUTO_CLAMPED", "Auto Clamped", ""), ("VECTOR", "Vector", ""), ("ALIGNED", "Aligned", ""), ("FREE", "Free", "")), description="Set handle type for the selected handle", default='AUTO', update=set_handle_type ) keyframe_numbers: BoolProperty(name="Keyframe numbers", description="Display keyframe numbers", default=False, update=internal_update ) mode: 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: 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: 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: BoolProperty(name="Path options", description="Display path options", default=True ) path_resolution: 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: 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 color", default='simple', update=internal_update ) path_transparency: IntProperty(name="Path transparency", description="Determines visibility of path", default=0, min=0, max=100, subtype='PERCENTAGE', update=internal_update ) path_width: IntProperty(name="Path width", description="Width in pixels", default=1, min=1, soft_max=5, update=internal_update ) timebeads: 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 cls in classes: bpy.utils.register_class(cls) bpy.types.WindowManager.motion_trail = PointerProperty( type=MotionTrailProps ) def unregister(): MotionTrailOperator.handle_remove() for cls in classes: bpy.utils.unregister_class(cls) del bpy.types.WindowManager.motion_trail if __name__ == "__main__": register()