3
11
This repository has been archived on 2024-05-16. You can view files and clone it, but cannot push or open issues or pull requests.
blender-addons-contrib/animation_motion_trail.py

1835 lines
77 KiB
Python

# ##### 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_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()