1835 lines
77 KiB
Python
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()
|