remove api field, was never used
[blender-addons-contrib.git] / animation_motion_trail.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
3 #  This program is free software; you can redistribute it and/or
4 #  modify it under the terms of the GNU General Public License
5 #  as published by the Free Software Foundation; either version 2
6 #  of the License, or (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software Foundation,
15 #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21
22 bl_info = {
23     'name': "Motion Trail",
24     'author': "Bart Crouch",
25     'version': (3, 1, 0),
26     'blender': (2, 6, 0),
27     'location': "View3D > Toolbar > Motion Trail tab",
28     'warning': "",
29     'description': "Display and edit motion trails in the 3d-view",
30     'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.5/"\
31         "Py/Scripts/Animation/Motion_Trail",
32     'tracker_url': "http://projects.blender.org/tracker/index.php?"\
33         "func=detail&aid=26374",
34     'category': 'Animation'}
35
36
37 import bgl
38 import blf
39 import bpy
40 from bpy_extras import view3d_utils
41 import math
42 import mathutils
43
44
45 # fake fcurve class, used if no fcurve is found for a path
46 class fake_fcurve():
47     def __init__(self, object, index, rotation=False, scale=False):
48         # location
49         if not rotation and not scale:
50             self.loc = object.location[index]
51         # scale
52         elif scale:
53             self.loc = object.scale[index]
54         # rotation
55         elif rotation == 'QUATERNION':
56             self.loc = object.rotation_quaternion[index]
57         elif rotation == 'AXIS_ANGLE':
58             self.loc = object.rotation_axis_angle[index]
59         else:
60             self.loc = object.rotation_euler[index]
61         self.keyframe_points = []
62     
63     def evaluate(self, frame):
64         return(self.loc)
65     
66     def range(self):
67         return([])
68
69
70 # get location curves of the given object
71 def get_curves(object, child=False):
72     if object.animation_data and object.animation_data.action:
73         action = object.animation_data.action
74         if child:
75             # posebone
76             curves = [fc for fc in action.fcurves if len(fc.data_path)>=14 \
77             and fc.data_path[-9:]=='.location' and \
78             child.name in fc.data_path.split("\"")]
79         else:
80             # normal object
81             curves = [fc for fc in action.fcurves if \
82             fc.data_path == 'location']
83     elif object.animation_data and object.animation_data.use_nla:
84         curves = []
85         strips = []
86         for track in object.animation_data.nla_tracks:
87             not_handled = [s for s in track.strips]
88             while not_handled:
89                 current_strip = not_handled.pop(-1)
90                 if current_strip.action:
91                     strips.append(current_strip)
92                 if current_strip.strips:
93                     # meta strip
94                     not_handled += [s for s in current_strip.strips]
95         
96         for strip in strips:
97             if child:
98                 # posebone
99                 curves = [fc for fc in strip.action.fcurves if \
100                 len(fc.data_path)>=14 and fc.data_path[-9:]=='.location' \
101                 and child.name in fc.data_path.split("\"")]
102             else:
103                 # normal object
104                 curves = [fc for fc in strip.action.fcurves if \
105                 fc.data_path == 'location']
106             if curves:
107                 # use first strip with location fcurves
108                 break
109     else:
110         # should not happen?
111         curves = []
112     
113     # ensure we have three curves per object
114     fcx = None
115     fcy = None
116     fcz = None
117     for fc in curves:
118         if fc.array_index == 0:
119             fcx = fc
120         elif fc.array_index == 1:
121             fcy = fc
122         elif fc.array_index == 2:
123             fcz = fc
124     if fcx == None:
125         fcx = fake_fcurve(object, 0)
126     if fcy == None:
127         fcy = fake_fcurve(object, 1)
128     if fcz == None:
129         fcz = fake_fcurve(object, 2)
130
131     return([fcx, fcy, fcz])
132
133
134 # turn screen coordinates (x,y) into world coordinates vector
135 def screen_to_world(context, x, y):
136     depth_vector = view3d_utils.region_2d_to_vector_3d(\
137         context.region, context.region_data, [x,y])
138     vector = view3d_utils.region_2d_to_location_3d(\
139         context.region, context.region_data, [x,y], depth_vector)
140     
141     return(vector)
142
143
144 # turn 3d world coordinates vector into screen coordinate integers (x,y)
145 def world_to_screen(context, vector):
146     prj = context.region_data.perspective_matrix * \
147         mathutils.Vector((vector[0], vector[1], vector[2], 1.0))
148     width_half = context.region.width / 2.0
149     height_half = context.region.height / 2.0
150
151     x = int(width_half + width_half * (prj.x / prj.w))
152     y = int(height_half + height_half * (prj.y / prj.w))
153     
154     # correction for corner cases in perspective mode
155     if prj.w < 0:
156         if x < 0:
157             x = context.region.width * 2
158         else:
159             x = context.region.width * -2
160         if y < 0:
161             y = context.region.height * 2
162         else:
163             y = context.region.height * -2
164     
165     return(x, y)
166
167
168 # calculate location of display_ob in worldspace
169 def get_location(frame, display_ob, offset_ob, curves):
170     if offset_ob:
171         bpy.context.scene.frame_set(frame)
172         display_mat = getattr(display_ob, "matrix", False)
173         if not display_mat:
174             # posebones have "matrix", objects have "matrix_world"
175             display_mat = display_ob.matrix_world
176         if offset_ob:
177             loc = display_mat.to_translation() + \
178                 offset_ob.matrix_world.to_translation()
179         else:
180             loc = display_mat.to_translation()
181     else:
182         fcx, fcy, fcz = curves
183         locx = fcx.evaluate(frame)
184         locy = fcy.evaluate(frame)
185         locz = fcz.evaluate(frame)
186         loc = mathutils.Vector([locx, locy, locz])
187     
188     return(loc)
189
190
191 # get position of keyframes and handles at the start of dragging
192 def get_original_animation_data(context, keyframes):
193     keyframes_ori = {}
194     handles_ori = {}
195     
196     if context.active_object and context.active_object.mode == 'POSE':
197         armature_ob = context.active_object
198         objects = [[armature_ob, pb, armature_ob] for pb in \
199             context.selected_pose_bones]
200     else:
201         objects = [[ob, False, False] for ob in context.selected_objects]
202     
203     for action_ob, child, offset_ob in objects:
204         if not action_ob.animation_data:
205             continue
206         curves = get_curves(action_ob, child)
207         if len(curves) == 0:
208             continue
209         fcx, fcy, fcz = curves
210         if child:
211             display_ob = child
212         else:
213             display_ob = action_ob
214         
215         # get keyframe positions
216         frame_old = context.scene.frame_current
217         keyframes_ori[display_ob.name] = {}
218         for frame in keyframes[display_ob.name]:
219             loc = get_location(frame, display_ob, offset_ob, curves)
220             keyframes_ori[display_ob.name][frame] = [frame, loc]
221         
222         # get handle positions
223         handles_ori[display_ob.name] = {}
224         for frame in keyframes[display_ob.name]:
225             handles_ori[display_ob.name][frame] = {}
226             left_x = [frame, fcx.evaluate(frame)]
227             right_x = [frame, fcx.evaluate(frame)]
228             for kf in fcx.keyframe_points:
229                 if kf.co[0] == frame:
230                     left_x = kf.handle_left[:]
231                     right_x = kf.handle_right[:]
232                     break
233             left_y = [frame, fcy.evaluate(frame)]
234             right_y = [frame, fcy.evaluate(frame)]
235             for kf in fcy.keyframe_points:
236                 if kf.co[0] == frame:
237                     left_y = kf.handle_left[:]
238                     right_y = kf.handle_right[:]
239                     break
240             left_z = [frame, fcz.evaluate(frame)]
241             right_z = [frame, fcz.evaluate(frame)]
242             for kf in fcz.keyframe_points:
243                 if kf.co[0] == frame:
244                     left_z = kf.handle_left[:]
245                     right_z = kf.handle_right[:]
246                     break
247             handles_ori[display_ob.name][frame]["left"] = [left_x, left_y,
248                 left_z]
249             handles_ori[display_ob.name][frame]["right"] = [right_x, right_y,
250                 right_z]
251         
252         if context.scene.frame_current != frame_old:
253             context.scene.frame_set(frame_old)
254     
255     return(keyframes_ori, handles_ori)
256
257
258 # callback function that calculates positions of all things that need be drawn
259 def calc_callback(self, context):
260     if context.active_object and context.active_object.mode == 'POSE':
261         armature_ob = context.active_object
262         objects = [[armature_ob, pb, armature_ob] for pb in \
263             context.selected_pose_bones]
264     else:
265         objects = [[ob, False, False] for ob in context.selected_objects]
266     if objects == self.displayed:
267         selection_change = False
268     else:
269         selection_change = True
270     
271     if self.lock and not selection_change and \
272     context.region_data.perspective_matrix == self.perspective and not \
273     context.window_manager.motion_trail.force_update:
274         return
275     
276     # dictionaries with key: objectname
277     self.paths = {} # value: list of lists with x, y, colour
278     self.keyframes = {} # value: dict with frame as key and [x,y] as value
279     self.handles = {} # value: dict of dicts
280     self.timebeads = {} # value: dict with frame as key and [x,y] as value
281     self.click = {} # value: list of lists with frame, type, loc-vector
282     if selection_change:
283         # value: editbone inverted rotation matrix or None
284         self.edit_bones = {}
285     if selection_change or not self.lock or context.window_manager.\
286     motion_trail.force_update:
287         # contains locations of path, keyframes and timebeads
288         self.cached = {"path":{}, "keyframes":{}, "timebeads_timing":{},
289             "timebeads_speed":{}}
290     if self.cached["path"]:
291         use_cache = True
292     else:
293         use_cache = False
294     self.perspective = context.region_data.perspective_matrix.copy()
295     self.displayed = objects # store, so it can be checked next time
296     context.window_manager.motion_trail.force_update = False
297    
298     global_undo = context.user_preferences.edit.use_global_undo
299     context.user_preferences.edit.use_global_undo = False
300     
301     for action_ob, child, offset_ob in objects:
302         if selection_change:
303             if not child:
304                 self.edit_bones[action_ob.name] = None
305             else:
306                 bpy.ops.object.mode_set(mode='EDIT')
307                 editbones = action_ob.data.edit_bones
308                 mat = editbones[child.name].matrix.copy().to_3x3().inverted()
309                 bpy.ops.object.mode_set(mode='POSE')
310                 self.edit_bones[child.name] = mat
311         
312         if not action_ob.animation_data:
313             continue
314         curves = get_curves(action_ob, child)
315         if len(curves) == 0:
316             continue
317         
318         if context.window_manager.motion_trail.path_before == 0:
319             range_min = context.scene.frame_start
320         else:
321             range_min = max(context.scene.frame_start,
322                 context.scene.frame_current - \
323                 context.window_manager.motion_trail.path_before)
324         if context.window_manager.motion_trail.path_after == 0:
325             range_max = context.scene.frame_end
326         else:
327             range_max = min(context.scene.frame_end,
328                 context.scene.frame_current + \
329                 context.window_manager.motion_trail.path_after)
330         fcx, fcy, fcz = curves
331         if child:
332             display_ob = child
333         else:
334             display_ob = action_ob
335         
336         # get location data of motion path
337         path = []
338         speeds = []
339         frame_old = context.scene.frame_current
340         step = 11 - context.window_manager.motion_trail.path_resolution
341         
342         if not use_cache:
343             if display_ob.name not in self.cached["path"]:
344                 self.cached["path"][display_ob.name] = {}
345         if use_cache and range_min-1 in self.cached["path"][display_ob.name]:
346             prev_loc = self.cached["path"][display_ob.name][range_min-1]
347         else:
348             prev_loc = get_location(range_min-1, display_ob, offset_ob, curves)
349             self.cached["path"][display_ob.name][range_min-1] = prev_loc
350         
351         for frame in range(range_min, range_max + 1, step):
352             if use_cache and frame in self.cached["path"][display_ob.name]:
353                 loc = self.cached["path"][display_ob.name][frame]
354             else:
355                 loc = get_location(frame, display_ob, offset_ob, curves)
356                 self.cached["path"][display_ob.name][frame] = loc
357             if not context.region or not context.space_data:
358                 continue
359             x, y = world_to_screen(context, loc)
360             if context.window_manager.motion_trail.path_style == 'simple':
361                 path.append([x, y, [0.0, 0.0, 0.0], frame, action_ob, child])
362             else:
363                 dloc = (loc - prev_loc).length
364                 path.append([x, y, dloc, frame, action_ob, child])
365                 speeds.append(dloc)
366                 prev_loc = loc
367         
368         # calculate colour of path
369         if context.window_manager.motion_trail.path_style == 'speed':
370             speeds.sort()
371             min_speed = speeds[0]
372             d_speed = speeds[-1] - min_speed
373             for i, [x, y, d_loc, frame, action_ob, child] in enumerate(path):
374                 relative_speed = (d_loc - min_speed) / d_speed # 0.0 to 1.0
375                 red = min(1.0, 2.0 * relative_speed)
376                 blue = min(1.0, 2.0 - (2.0 * relative_speed))
377                 path[i][2] = [red, 0.0, blue]
378         elif context.window_manager.motion_trail.path_style == 'acceleration':
379             accelerations = []
380             prev_speed = 0.0
381             for i, [x, y, d_loc, frame, action_ob, child] in enumerate(path):
382                 accel = d_loc - prev_speed
383                 accelerations.append(accel)
384                 path[i][2] = accel
385                 prev_speed = d_loc
386             accelerations.sort()
387             min_accel = accelerations[0]
388             max_accel = accelerations[-1]
389             for i, [x, y, accel, frame, action_ob, child] in enumerate(path):
390                 if accel < 0:
391                     relative_accel = accel / min_accel # values from 0.0 to 1.0
392                     green = 1.0 - relative_accel
393                     path[i][2] = [1.0, green, 0.0]
394                 elif accel > 0:
395                     relative_accel = accel / max_accel # values from 0.0 to 1.0
396                     red = 1.0 - relative_accel
397                     path[i][2] = [red, 1.0, 0.0]
398                 else:
399                     path[i][2] = [1.0, 1.0, 0.0]
400         self.paths[display_ob.name] = path
401         
402         # get keyframes and handles
403         keyframes = {}
404         handle_difs = {}
405         kf_time = []
406         click = []
407         if not use_cache:
408             if display_ob.name not in self.cached["keyframes"]:
409                 self.cached["keyframes"][display_ob.name] = {}
410         
411         for fc in curves:
412             for kf in fc.keyframe_points:
413                 # handles for location mode
414                 if context.window_manager.motion_trail.mode == 'location':
415                     if kf.co[0] not in handle_difs:
416                         handle_difs[kf.co[0]] = {"left":mathutils.Vector(),
417                             "right":mathutils.Vector(), "keyframe_loc":None}
418                     handle_difs[kf.co[0]]["left"][fc.array_index] = \
419                         (mathutils.Vector(kf.handle_left[:]) - \
420                         mathutils.Vector(kf.co[:])).normalized()[1]
421                     handle_difs[kf.co[0]]["right"][fc.array_index] = \
422                         (mathutils.Vector(kf.handle_right[:]) - \
423                         mathutils.Vector(kf.co[:])).normalized()[1]
424                 # keyframes
425                 if kf.co[0] in kf_time:
426                     continue
427                 kf_time.append(kf.co[0])
428                 co = kf.co[0]
429                 
430                 if use_cache and co in \
431                 self.cached["keyframes"][display_ob.name]:
432                     loc = self.cached["keyframes"][display_ob.name][co]
433                 else:
434                     loc = get_location(co, display_ob, offset_ob, curves)
435                     self.cached["keyframes"][display_ob.name][co] = loc
436                 if handle_difs:
437                     handle_difs[co]["keyframe_loc"] = loc
438                 
439                 x, y = world_to_screen(context, loc)
440                 keyframes[kf.co[0]] = [x, y]
441                 if context.window_manager.motion_trail.mode != 'speed':
442                     # can't select keyframes in speed mode
443                     click.append([kf.co[0], "keyframe",
444                         mathutils.Vector([x,y]), action_ob, child])
445         self.keyframes[display_ob.name] = keyframes
446         
447         # handles are only shown in location-altering mode
448         if context.window_manager.motion_trail.mode == 'location' and \
449         context.window_manager.motion_trail.handle_display:
450             # calculate handle positions
451             handles = {}
452             for frame, vecs in handle_difs.items():
453                 if child:
454                     # bone space to world space
455                     mat = self.edit_bones[child.name].copy().inverted()
456                     vec_left = vecs["left"] * mat
457                     vec_right = vecs["right"] * mat
458                 else:
459                     vec_left = vecs["left"]
460                     vec_right = vecs["right"]
461                 if vecs["keyframe_loc"] != None:
462                     vec_keyframe = vecs["keyframe_loc"]
463                 else:
464                     vec_keyframe = get_location(frame, display_ob, offset_ob,
465                         curves)
466                 x_left, y_left = world_to_screen(context, vec_left*2 + \
467                     vec_keyframe)
468                 x_right, y_right = world_to_screen(context, vec_right*2 + \
469                     vec_keyframe)
470                 handles[frame] = {"left":[x_left, y_left],
471                     "right":[x_right, y_right]}
472                 click.append([frame, "handle_left",
473                     mathutils.Vector([x_left, y_left]), action_ob, child])
474                 click.append([frame, "handle_right",
475                     mathutils.Vector([x_right, y_right]), action_ob, child])
476             self.handles[display_ob.name] = handles
477         
478         # calculate timebeads for timing mode
479         if context.window_manager.motion_trail.mode == 'timing':
480             timebeads = {}
481             n = context.window_manager.motion_trail.timebeads * (len(kf_time) \
482                 - 1)
483             dframe = (range_max - range_min) / (n + 1)
484             if not use_cache:
485                 if display_ob.name not in self.cached["timebeads_timing"]:
486                     self.cached["timebeads_timing"][display_ob.name] = {}
487             
488             for i in range(1, n+1):
489                 frame = range_min + i * dframe
490                 if use_cache and frame in \
491                 self.cached["timebeads_timing"][display_ob.name]:
492                     loc = self.cached["timebeads_timing"][display_ob.name]\
493                         [frame]
494                 else:
495                     loc = get_location(frame, display_ob, offset_ob, curves)
496                     self.cached["timebeads_timing"][display_ob.name][frame] = \
497                         loc
498                 x, y = world_to_screen(context, loc)
499                 timebeads[frame] = [x, y]
500                 click.append([frame, "timebead", mathutils.Vector([x,y]),
501                     action_ob, child])
502             self.timebeads[display_ob.name] = timebeads
503         
504         # calculate timebeads for speed mode
505         if context.window_manager.motion_trail.mode == 'speed':
506             angles = dict([[kf, {"left":[], "right":[]}] for kf in \
507                 self.keyframes[display_ob.name]])
508             for fc in curves:
509                 for i, kf in enumerate(fc.keyframe_points):
510                     if i != 0:
511                         angle = mathutils.Vector([-1, 0]).angle(mathutils.\
512                             Vector(kf.handle_left) - mathutils.Vector(kf.co),
513                             0)
514                         if angle != 0:
515                             angles[kf.co[0]]["left"].append(angle)
516                     if i != len(fc.keyframe_points) - 1:
517                         angle = mathutils.Vector([1, 0]).angle(mathutils.\
518                             Vector(kf.handle_right) - mathutils.Vector(kf.co),
519                             0)
520                         if angle != 0:
521                             angles[kf.co[0]]["right"].append(angle)
522             timebeads = {}
523             kf_time.sort()
524             if not use_cache:
525                 if display_ob.name not in self.cached["timebeads_speed"]:
526                     self.cached["timebeads_speed"][display_ob.name] = {}
527             
528             for frame, sides in angles.items():
529                 if sides["left"]:
530                     perc = (sum(sides["left"]) / len(sides["left"])) / \
531                         (math.pi / 2)
532                     perc = max(0.4, min(1, perc * 5))
533                     previous = kf_time[kf_time.index(frame) - 1]
534                     bead_frame = frame - perc * ((frame - previous - 2) / 2)
535                     if use_cache and bead_frame in \
536                     self.cached["timebeads_speed"][display_ob.name]:
537                         loc = self.cached["timebeads_speed"][display_ob.name]\
538                             [bead_frame]
539                     else:
540                         loc = get_location(bead_frame, display_ob, offset_ob,
541                             curves)
542                         self.cached["timebeads_speed"][display_ob.name]\
543                             [bead_frame] = loc
544                     x, y = world_to_screen(context, loc)
545                     timebeads[bead_frame] = [x, y]
546                     click.append([bead_frame, "timebead", mathutils.\
547                         Vector([x,y]), action_ob, child])
548                 if sides["right"]:
549                     perc = (sum(sides["right"]) / len(sides["right"])) / \
550                         (math.pi / 2)
551                     perc = max(0.4, min(1, perc * 5))
552                     next = kf_time[kf_time.index(frame) + 1]
553                     bead_frame = frame + perc * ((next - frame - 2) / 2)
554                     if use_cache and bead_frame in \
555                     self.cached["timebeads_speed"][display_ob.name]:
556                         loc = self.cached["timebeads_speed"][display_ob.name]\
557                             [bead_frame]
558                     else:
559                         loc = get_location(bead_frame, display_ob, offset_ob,
560                             curves)
561                         self.cached["timebeads_speed"][display_ob.name]\
562                             [bead_frame] = loc
563                     x, y = world_to_screen(context, loc)
564                     timebeads[bead_frame] = [x, y]
565                     click.append([bead_frame, "timebead", mathutils.\
566                         Vector([x,y]), action_ob, child])
567             self.timebeads[display_ob.name] = timebeads
568         
569         # add frame positions to click-list
570         if context.window_manager.motion_trail.frame_display:
571             path = self.paths[display_ob.name]
572             for x, y, colour, frame, action_ob, child in path:
573                 click.append([frame, "frame", mathutils.Vector([x,y]),
574                     action_ob, child])
575         
576         self.click[display_ob.name] = click
577         
578         if context.scene.frame_current != frame_old:
579             context.scene.frame_set(frame_old)
580     
581     context.user_preferences.edit.use_global_undo = global_undo
582
583
584 # draw in 3d-view
585 def draw_callback(self, context):
586     # polling
587     if context.mode not in ['OBJECT', 'POSE'] or \
588     context.window_manager.motion_trail.enabled != 1:
589         return
590     
591     # display limits
592     if context.window_manager.motion_trail.path_before != 0:
593         limit_min = context.scene.frame_current - \
594             context.window_manager.motion_trail.path_before
595     else:
596         limit_min = -1e6
597     if context.window_manager.motion_trail.path_after != 0:
598         limit_max = context.scene.frame_current + \
599             context.window_manager.motion_trail.path_after
600     else:
601         limit_max = 1e6
602     
603     # draw motion path
604     bgl.glEnable(bgl.GL_BLEND)
605     bgl.glLineWidth(context.window_manager.motion_trail.path_width)
606     alpha = 1.0 - (context.window_manager.motion_trail.path_transparency / \
607         100.0)
608     if context.window_manager.motion_trail.path_style == 'simple':
609         bgl.glColor4f(0.0, 0.0, 0.0, alpha)
610         for objectname, path in self.paths.items():
611             bgl.glBegin(bgl.GL_LINE_STRIP)
612             for x, y, colour, frame, action_ob, child in path:
613                 if frame < limit_min or frame > limit_max:
614                     continue
615                 bgl.glVertex2i(x, y)
616             bgl.glEnd()
617     else:
618         for objectname, path in self.paths.items():
619             for i, [x, y, colour, frame, action_ob, child] in enumerate(path):
620                 if frame < limit_min or frame > limit_max:
621                     continue
622                 r, g, b = colour
623                 if i != 0:
624                     prev_path = path[i-1]
625                     halfway = [(x + prev_path[0])/2, (y + prev_path[1])/2]
626                     bgl.glColor4f(r, g, b, alpha)
627                     bgl.glBegin(bgl.GL_LINE_STRIP)
628                     bgl.glVertex2i(int(halfway[0]), int(halfway[1]))
629                     bgl.glVertex2i(x, y)
630                     bgl.glEnd()
631                 if i != len(path) - 1:
632                     next_path = path[i+1]
633                     halfway = [(x + next_path[0])/2, (y + next_path[1])/2]
634                     bgl.glColor4f(r, g, b, alpha)
635                     bgl.glBegin(bgl.GL_LINE_STRIP)
636                     bgl.glVertex2i(x, y)
637                     bgl.glVertex2i(int(halfway[0]), int(halfway[1]))
638                     bgl.glEnd()
639     
640     # draw frames
641     if context.window_manager.motion_trail.frame_display:
642         bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
643         bgl.glPointSize(1)
644         bgl.glBegin(bgl.GL_POINTS)
645         for objectname, path in self.paths.items():
646             for x, y, colour, frame, action_ob, child in path:
647                 if frame < limit_min or frame > limit_max:
648                     continue
649                 if self.active_frame and objectname == self.active_frame[0] \
650                 and abs(frame - self.active_frame[1]) < 1e-4:
651                     bgl.glEnd()
652                     bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
653                     bgl.glPointSize(3)
654                     bgl.glBegin(bgl.GL_POINTS)
655                     bgl.glVertex2i(x,y)
656                     bgl.glEnd()
657                     bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
658                     bgl.glPointSize(1)
659                     bgl.glBegin(bgl.GL_POINTS)
660                 else:
661                     bgl.glVertex2i(x,y)
662         bgl.glEnd()
663     
664     # time beads are shown in speed and timing modes
665     if context.window_manager.motion_trail.mode in ['speed', 'timing']:
666         bgl.glColor4f(0.0, 1.0, 0.0, 1.0)
667         bgl.glPointSize(4)
668         bgl.glBegin(bgl.GL_POINTS)
669         for objectname, values in self.timebeads.items():
670             for frame, coords in values.items():
671                 if frame < limit_min or frame > limit_max:
672                     continue
673                 if self.active_timebead and \
674                 objectname == self.active_timebead[0] and \
675                 abs(frame - self.active_timebead[1]) < 1e-4:
676                     bgl.glEnd()
677                     bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
678                     bgl.glBegin(bgl.GL_POINTS)
679                     bgl.glVertex2i(coords[0], coords[1])
680                     bgl.glEnd()
681                     bgl.glColor4f(0.0, 1.0, 0.0, 1.0)
682                     bgl.glBegin(bgl.GL_POINTS)
683                 else:
684                     bgl.glVertex2i(coords[0], coords[1])
685         bgl.glEnd()
686     
687     # handles are only shown in location mode
688     if context.window_manager.motion_trail.mode == 'location':
689         # draw handle-lines
690         bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
691         bgl.glLineWidth(1)
692         bgl.glBegin(bgl.GL_LINES)
693         for objectname, values in self.handles.items():
694             for frame, sides in values.items():
695                 if frame < limit_min or frame > limit_max:
696                     continue
697                 for side, coords in sides.items():
698                     if self.active_handle and \
699                     objectname == self.active_handle[0] and \
700                     side == self.active_handle[2] and \
701                     abs(frame - self.active_handle[1]) < 1e-4:
702                         bgl.glEnd()
703                         bgl.glColor4f(.75, 0.25, 0.0, 1.0)
704                         bgl.glBegin(bgl.GL_LINES)
705                         bgl.glVertex2i(self.keyframes[objectname][frame][0],
706                             self.keyframes[objectname][frame][1])
707                         bgl.glVertex2i(coords[0], coords[1])
708                         bgl.glEnd()
709                         bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
710                         bgl.glBegin(bgl.GL_LINES)
711                     else:
712                         bgl.glVertex2i(self.keyframes[objectname][frame][0],
713                             self.keyframes[objectname][frame][1])
714                         bgl.glVertex2i(coords[0], coords[1])
715         bgl.glEnd()
716         
717         # draw handles
718         bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
719         bgl.glPointSize(4)
720         bgl.glBegin(bgl.GL_POINTS)
721         for objectname, values in self.handles.items():
722             for frame, sides in values.items():
723                 if frame < limit_min or frame > limit_max:
724                     continue
725                 for side, coords in sides.items():
726                     if self.active_handle and \
727                     objectname == self.active_handle[0] and \
728                     side == self.active_handle[2] and \
729                     abs(frame - self.active_handle[1]) < 1e-4:
730                         bgl.glEnd()
731                         bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
732                         bgl.glBegin(bgl.GL_POINTS)
733                         bgl.glVertex2i(coords[0], coords[1])
734                         bgl.glEnd()
735                         bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
736                         bgl.glBegin(bgl.GL_POINTS)
737                     else:
738                         bgl.glVertex2i(coords[0], coords[1])
739         bgl.glEnd()
740         
741     # draw keyframes
742     bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
743     bgl.glPointSize(6)
744     bgl.glBegin(bgl.GL_POINTS)
745     for objectname, values in self.keyframes.items():
746         for frame, coords in values.items():
747             if frame < limit_min or frame > limit_max:
748                 continue
749             if self.active_keyframe and \
750             objectname == self.active_keyframe[0] and \
751             abs(frame - self.active_keyframe[1]) < 1e-4:
752                 bgl.glEnd()
753                 bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
754                 bgl.glBegin(bgl.GL_POINTS)
755                 bgl.glVertex2i(coords[0], coords[1])
756                 bgl.glEnd()
757                 bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
758                 bgl.glBegin(bgl.GL_POINTS)
759             else:
760                 bgl.glVertex2i(coords[0], coords[1])
761     bgl.glEnd()
762     
763     # draw keyframe-numbers
764     if context.window_manager.motion_trail.keyframe_numbers:
765         blf.size(0, 12, 72)
766         bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
767         for objectname, values in self.keyframes.items():
768             for frame, coords in values.items():
769                 if frame < limit_min or frame > limit_max:
770                     continue
771                 blf.position(0, coords[0] + 3, coords[1] + 3, 0)
772                 text = str(frame).split(".")
773                 if len(text) == 1:
774                     text = text[0]
775                 elif len(text[1]) == 1 and text[1] == "0":
776                     text = text[0]
777                 else:
778                     text = text[0] + "." + text[1][0]
779                 if self.active_keyframe and \
780                 objectname == self.active_keyframe[0] and \
781                 abs(frame - self.active_keyframe[1]) < 1e-4:
782                     bgl.glColor4f(1.0, 0.5, 0.0, 1.0)
783                     blf.draw(0, text)
784                     bgl.glColor4f(1.0, 1.0, 0.0, 1.0)
785                 else:
786                     blf.draw(0, text)
787     
788     # restore opengl defaults
789     bgl.glLineWidth(1)
790     bgl.glDisable(bgl.GL_BLEND)
791     bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
792     bgl.glPointSize(1)
793
794
795 # change data based on mouse movement
796 def drag(context, event, drag_mouse_ori, active_keyframe, active_handle,
797 active_timebead, keyframes_ori, handles_ori, edit_bones):
798     # change 3d-location of keyframe
799     if context.window_manager.motion_trail.mode == 'location' and \
800     active_keyframe:
801         objectname, frame, frame_ori, action_ob, child = active_keyframe
802         if child:
803             mat = action_ob.matrix_world.copy().inverted() * \
804                 edit_bones[child.name].copy().to_4x4()
805         else:
806             mat = 1
807         
808         mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
809             drag_mouse_ori[1]) * mat
810         vector = screen_to_world(context, event.mouse_region_x,
811             event.mouse_region_y) * mat
812         d = vector - mouse_ori_world
813         
814         loc_ori_ws = keyframes_ori[objectname][frame][1]
815         loc_ori_bs = loc_ori_ws * mat
816         new_loc = loc_ori_bs + d
817         curves = get_curves(action_ob, child)
818         
819         for i, curve in enumerate(curves):
820             for kf in curve.keyframe_points:
821                 if kf.co[0] == frame:
822                     kf.co[1] = new_loc[i]
823                     kf.handle_left[1] = handles_ori[objectname][frame]\
824                         ["left"][i][1] + d[i]
825                     kf.handle_right[1] = handles_ori[objectname][frame]\
826                         ["right"][i][1] + d[i]
827                     break
828     
829     # change 3d-location of handle
830     elif context.window_manager.motion_trail.mode == 'location' and \
831     active_handle:
832         objectname, frame, side, action_ob, child = active_handle
833         if child:
834             mat = action_ob.matrix_world.copy().inverted() * \
835                 edit_bones[child.name].copy().to_4x4()
836         else:
837             mat = 1
838         
839         mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
840             drag_mouse_ori[1]) * mat
841         vector = screen_to_world(context, event.mouse_region_x,
842             event.mouse_region_y) * mat
843         d = vector - mouse_ori_world
844         curves = get_curves(action_ob, child)
845         
846         for i, curve in enumerate(curves):
847             for kf in curve.keyframe_points:
848                 if kf.co[0] == frame:
849                     if side == "left":
850                         # change handle type, if necessary
851                         if kf.handle_left_type in ['AUTO', 'ANIM_CLAMPED']:
852                             kf.handle_left_type = 'ALIGNED'
853                         elif kf.handle_left_type == 'VECTOR':
854                             kf.handle_left_type = 'FREE'
855                         # change handle position(s)
856                         kf.handle_left[1] = handles_ori[objectname][frame]\
857                             ["left"][i][1] + d[i]
858                         if kf.handle_left_type in ['ALIGNED', 'ANIM_CLAMPED',
859                         'AUTO']:
860                             dif = (abs(handles_ori[objectname][frame]["right"]\
861                                 [i][0] - kf.co[0]) / abs(kf.handle_left[0] - \
862                                 kf.co[0])) * d[i]
863                             kf.handle_right[1] = handles_ori[objectname]\
864                                 [frame]["right"][i][1] - dif
865                     elif side == "right":
866                         # change handle type, if necessary
867                         if kf.handle_right_type in ['AUTO', 'ANIM_CLAMPED']:
868                             kf.handle_left_type = 'ALIGNED'
869                             kf.handle_right_type = 'ALIGNED'
870                         elif kf.handle_right_type == 'VECTOR':
871                             kf.handle_left_type = 'FREE'
872                             kf.handle_right_type = 'FREE'
873                         # change handle position(s)
874                         kf.handle_right[1] = handles_ori[objectname][frame]\
875                             ["right"][i][1] + d[i]
876                         if kf.handle_right_type in ['ALIGNED', 'ANIM_CLAMPED',
877                         'AUTO']:
878                             dif = (abs(handles_ori[objectname][frame]["left"]\
879                                 [i][0] - kf.co[0]) / abs(kf.handle_right[0] - \
880                                 kf.co[0])) * d[i]
881                             kf.handle_left[1] = handles_ori[objectname]\
882                                 [frame]["left"][i][1] - dif
883                     break
884     
885     # change position of all keyframes on timeline
886     elif context.window_manager.motion_trail.mode == 'timing' and \
887     active_timebead:
888         objectname, frame, frame_ori, action_ob, child = active_timebead
889         curves = get_curves(action_ob, child)
890         ranges = [val for c in curves for val in c.range()]
891         ranges.sort()
892         range_min = round(ranges[0])
893         range_max = round(ranges[-1])
894         range = range_max - range_min
895         dx_screen = -(mathutils.Vector([event.mouse_region_x,
896             event.mouse_region_y]) - drag_mouse_ori)[0]
897         dx_screen = dx_screen / context.region.width * range
898         new_frame = frame + dx_screen
899         shift_low = max(1e-4, (new_frame - range_min) / (frame - range_min))
900         shift_high = max(1e-4, (range_max - new_frame) / (range_max - frame))
901         
902         new_mapping = {}
903         for i, curve in enumerate(curves):
904             for j, kf in enumerate(curve.keyframe_points):
905                 frame_map = kf.co[0]
906                 if frame_map < range_min + 1e-4 or \
907                 frame_map > range_max - 1e-4:
908                     continue
909                 frame_ori = False
910                 for f in keyframes_ori[objectname]:
911                     if abs(f - frame_map) < 1e-4:
912                         frame_ori = keyframes_ori[objectname][f][0]
913                         value_ori = keyframes_ori[objectname][f]
914                         break
915                 if not frame_ori:
916                     continue
917                 if frame_ori <= frame:
918                     frame_new = (frame_ori - range_min) * shift_low + \
919                         range_min
920                 else:
921                     frame_new = range_max - (range_max - frame_ori) * \
922                         shift_high
923                 frame_new = max(range_min + j, min(frame_new, range_max - \
924                     (len(curve.keyframe_points)-j)+1))
925                 d_frame = frame_new - frame_ori
926                 if frame_new not in new_mapping:
927                     new_mapping[frame_new] = value_ori
928                 kf.co[0] = frame_new
929                 kf.handle_left[0] = handles_ori[objectname][frame_ori]\
930                     ["left"][i][0] + d_frame
931                 kf.handle_right[0] = handles_ori[objectname][frame_ori]\
932                     ["right"][i][0] + d_frame
933         del keyframes_ori[objectname]
934         keyframes_ori[objectname] = {}
935         for new_frame, value in new_mapping.items():
936             keyframes_ori[objectname][new_frame] = value
937     
938     # change position of active keyframe on the timeline
939     elif context.window_manager.motion_trail.mode == 'timing' and \
940     active_keyframe:
941         objectname, frame, frame_ori, action_ob, child = active_keyframe
942         if child:
943             mat = action_ob.matrix_world.copy().inverted() * \
944                 edit_bones[child.name].copy().to_4x4()
945         else:
946             mat = action_ob.matrix_world.copy().inverted()
947         
948         mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
949             drag_mouse_ori[1]) * mat
950         vector = screen_to_world(context, event.mouse_region_x,
951             event.mouse_region_y) * mat
952         d = vector - mouse_ori_world
953         
954         locs_ori = [[f_ori, coords] for f_mapped, [f_ori, coords] in \
955             keyframes_ori[objectname].items()]
956         locs_ori.sort()
957         direction = 1
958         range = False
959         for i, [f_ori, coords] in enumerate(locs_ori):
960             if abs(frame_ori - f_ori) < 1e-4:
961                 if i == 0:
962                     # first keyframe, nothing before it
963                     direction = -1
964                     range = [f_ori, locs_ori[i+1][0]]
965                 elif i == len(locs_ori) - 1:
966                     # last keyframe, nothing after it
967                     range = [locs_ori[i-1][0], f_ori]
968                 else:
969                     current = mathutils.Vector(coords)
970                     next = mathutils.Vector(locs_ori[i+1][1])
971                     previous = mathutils.Vector(locs_ori[i-1][1])
972                     angle_to_next = d.angle(next - current, 0)
973                     angle_to_previous = d.angle(previous-current, 0)
974                     if angle_to_previous < angle_to_next:
975                         # mouse movement is in direction of previous keyframe
976                         direction = -1
977                     range = [locs_ori[i-1][0], locs_ori[i+1][0]]
978                 break
979         direction *= -1 # feels more natural in 3d-view
980         if not range:
981             # keyframe not found, is impossible, but better safe than sorry
982             return(active_keyframe, active_timebead, keyframes_ori)
983         # calculate strength of movement
984         d_screen = mathutils.Vector([event.mouse_region_x,
985             event.mouse_region_y]) - drag_mouse_ori
986         if d_screen.length != 0:
987             d_screen = d_screen.length / (abs(d_screen[0])/d_screen.length*\
988                 context.region.width + abs(d_screen[1])/d_screen.length*\
989                 context.region.height)
990             d_screen *= direction  # d_screen value ranges from -1.0 to 1.0
991         else:
992             d_screen = 0.0
993         new_frame = d_screen * (range[1] - range[0]) + frame_ori
994         max_frame = range[1]
995         if max_frame == frame_ori:
996             max_frame += 1
997         min_frame = range[0]
998         if min_frame == frame_ori:
999             min_frame -= 1
1000         new_frame = min(max_frame - 1, max(min_frame + 1, new_frame))
1001         d_frame = new_frame - frame_ori
1002         curves = get_curves(action_ob, child)
1003         
1004         for i, curve in enumerate(curves):
1005             for kf in curve.keyframe_points:
1006                 if abs(kf.co[0] - frame) < 1e-4:
1007                     kf.co[0] = new_frame
1008                     kf.handle_left[0] = handles_ori[objectname][frame_ori]\
1009                         ["left"][i][0] + d_frame
1010                     kf.handle_right[0] = handles_ori[objectname][frame_ori]\
1011                         ["right"][i][0] + d_frame
1012                     break
1013         active_keyframe = [objectname, new_frame, frame_ori, action_ob, child]
1014     
1015     # change position of active timebead on the timeline, thus altering speed
1016     elif context.window_manager.motion_trail.mode == 'speed' and \
1017     active_timebead:
1018         objectname, frame, frame_ori, action_ob, child = active_timebead
1019         if child:
1020             mat = action_ob.matrix_world.copy().inverted() * \
1021                 edit_bones[child.name].copy().to_4x4()
1022         else:
1023             mat = 1
1024         
1025         mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
1026             drag_mouse_ori[1]) * mat
1027         vector = screen_to_world(context, event.mouse_region_x,
1028             event.mouse_region_y) * mat
1029         d = vector - mouse_ori_world
1030         
1031         # determine direction (to next or previous keyframe)
1032         curves = get_curves(action_ob, child)
1033         fcx, fcy, fcz = curves
1034         locx = fcx.evaluate(frame_ori)
1035         locy = fcy.evaluate(frame_ori)
1036         locz = fcz.evaluate(frame_ori)
1037         loc_ori = mathutils.Vector([locx, locy, locz]) # bonespace
1038         keyframes = [kf for kf in keyframes_ori[objectname]]
1039         keyframes.append(frame_ori)
1040         keyframes.sort()
1041         frame_index = keyframes.index(frame_ori)
1042         kf_prev = keyframes[frame_index - 1]
1043         kf_next = keyframes[frame_index + 1]
1044         vec_prev = (mathutils.Vector(keyframes_ori[objectname][kf_prev][1]) \
1045             * mat - loc_ori).normalized()
1046         vec_next = (mathutils.Vector(keyframes_ori[objectname][kf_next][1]) \
1047             * mat - loc_ori).normalized()
1048         d_normal = d.copy().normalized()
1049         dist_to_next = (d_normal - vec_next).length
1050         dist_to_prev = (d_normal - vec_prev).length
1051         if dist_to_prev < dist_to_next:
1052             direction = 1
1053         else:
1054             direction = -1
1055         
1056         if (kf_next - frame_ori) < (frame_ori - kf_prev):
1057             kf_bead = kf_next
1058             side = "left"
1059         else:
1060             kf_bead = kf_prev
1061             side = "right"
1062         d_frame = d.length * direction * 2 # *2 to make it more sensitive
1063         
1064         angles = []
1065         for i, curve in enumerate(curves):
1066             for kf in curve.keyframe_points:
1067                 if abs(kf.co[0] - kf_bead) < 1e-4:
1068                     if side == "left":
1069                         # left side
1070                         kf.handle_left[0] = min(handles_ori[objectname]\
1071                             [kf_bead]["left"][i][0] + d_frame, kf_bead - 1)
1072                         angle = mathutils.Vector([-1, 0]).angle(mathutils.\
1073                             Vector(kf.handle_left) - mathutils.Vector(kf.co),
1074                             0)
1075                         if angle != 0:
1076                             angles.append(angle)
1077                     else:
1078                         # right side
1079                         kf.handle_right[0] = max(handles_ori[objectname]\
1080                             [kf_bead]["right"][i][0] + d_frame, kf_bead + 1)
1081                         angle = mathutils.Vector([1, 0]).angle(mathutils.\
1082                             Vector(kf.handle_right) - mathutils.Vector(kf.co),
1083                             0)
1084                         if angle != 0:
1085                             angles.append(angle)
1086                     break
1087         
1088         # update frame of active_timebead
1089         perc = (sum(angles) / len(angles)) / (math.pi / 2)
1090         perc = max(0.4, min(1, perc * 5))
1091         if side == "left":
1092             bead_frame = kf_bead - perc * ((kf_bead - kf_prev - 2) / 2)
1093         else:
1094             bead_frame = kf_bead + perc * ((kf_next - kf_bead - 2) / 2)
1095         active_timebead = [objectname, bead_frame, frame_ori, action_ob, child]
1096     
1097     return(active_keyframe, active_timebead, keyframes_ori)
1098
1099
1100 # revert changes made by dragging
1101 def cancel_drag(context, active_keyframe, active_handle, active_timebead,
1102 keyframes_ori, handles_ori, edit_bones):
1103     # revert change in 3d-location of active keyframe and its handles
1104     if context.window_manager.motion_trail.mode == 'location' and \
1105     active_keyframe:
1106         objectname, frame, frame_ori, active_ob, child = active_keyframe
1107         curves = get_curves(active_ob, child)
1108         loc_ori = keyframes_ori[objectname][frame][1]
1109         if child:
1110             loc_ori = loc_ori * edit_bones[child.name] * \
1111                 active_ob.matrix_world.copy().inverted()
1112         for i, curve in enumerate(curves):
1113             for kf in curve.keyframe_points:
1114                 if kf.co[0] == frame:
1115                     kf.co[1] = loc_ori[i]
1116                     kf.handle_left[1] = handles_ori[objectname][frame]\
1117                         ["left"][i][1]
1118                     kf.handle_right[1] = handles_ori[objectname][frame]\
1119                         ["right"][i][1]
1120                     break
1121     
1122     # revert change in 3d-location of active handle
1123     elif context.window_manager.motion_trail.mode == 'location' and \
1124     active_handle:
1125         objectname, frame, side, active_ob, child = active_handle
1126         curves = get_curves(active_ob, child)
1127         for i, curve in enumerate(curves):
1128             for kf in curve.keyframe_points:
1129                 if kf.co[0] == frame:
1130                     kf.handle_left[1] = handles_ori[objectname][frame]\
1131                         ["left"][i][1]
1132                     kf.handle_right[1] = handles_ori[objectname][frame]\
1133                         ["right"][i][1]
1134                     break
1135     
1136     # revert position of all keyframes and handles on timeline
1137     elif context.window_manager.motion_trail.mode == 'timing' and \
1138     active_timebead:
1139         objectname, frame, frame_ori, active_ob, child = active_timebead
1140         curves = get_curves(active_ob, child)
1141         for i, curve in enumerate(curves):
1142             for kf in curve.keyframe_points:
1143                 for kf_ori, [frame_ori, loc] in keyframes_ori[objectname].\
1144                 items():
1145                     if abs(kf.co[0] - kf_ori) < 1e-4:
1146                         kf.co[0] = frame_ori
1147                         kf.handle_left[0] = handles_ori[objectname]\
1148                             [frame_ori]["left"][i][0]
1149                         kf.handle_right[0] = handles_ori[objectname]\
1150                             [frame_ori]["right"][i][0]
1151                         break
1152     
1153     # revert position of active keyframe and its handles on the timeline
1154     elif context.window_manager.motion_trail.mode == 'timing' and \
1155     active_keyframe:
1156         objectname, frame, frame_ori, active_ob, child = active_keyframe
1157         curves = get_curves(active_ob, child)
1158         for i, curve in enumerate(curves):
1159             for kf in curve.keyframe_points:
1160                 if abs(kf.co[0] - frame) < 1e-4:
1161                     kf.co[0] = keyframes_ori[objectname][frame_ori][0]
1162                     kf.handle_left[0] = handles_ori[objectname][frame_ori]\
1163                         ["left"][i][0]
1164                     kf.handle_right[0] = handles_ori[objectname][frame_ori]\
1165                         ["right"][i][0]
1166                     break
1167         active_keyframe = [objectname, frame_ori, frame_ori, active_ob, child]
1168     
1169     # revert position of handles on the timeline
1170     elif context.window_manager.motion_trail.mode == 'speed' and \
1171     active_timebead:
1172         objectname, frame, frame_ori, active_ob, child = active_timebead
1173         curves = get_curves(active_ob, child)
1174         keyframes = [kf for kf in keyframes_ori[objectname]]
1175         keyframes.append(frame_ori)
1176         keyframes.sort()
1177         frame_index = keyframes.index(frame_ori)
1178         kf_prev = keyframes[frame_index - 1]
1179         kf_next = keyframes[frame_index + 1]
1180         if (kf_next - frame_ori) < (frame_ori - kf_prev):
1181             kf_frame = kf_next
1182         else:
1183             kf_frame = kf_prev
1184         for i, curve in enumerate(curves):
1185             for kf in curve.keyframe_points:
1186                 if kf.co[0] == kf_frame:
1187                     kf.handle_left[0] = handles_ori[objectname][kf_frame]\
1188                         ["left"][i][0]
1189                     kf.handle_right[0] = handles_ori[objectname][kf_frame]\
1190                         ["right"][i][0]
1191                     break
1192         active_timebead = [objectname, frame_ori, frame_ori, active_ob, child]
1193     
1194     return(active_keyframe, active_timebead)
1195
1196
1197 # return the handle type of the active selection
1198 def get_handle_type(active_keyframe, active_handle):
1199     if active_keyframe:
1200         objectname, frame, side, action_ob, child = active_keyframe
1201         side = "both"
1202     elif active_handle:
1203         objectname, frame, side, action_ob, child = active_handle
1204     else:
1205         # no active handle(s)
1206         return(False)
1207     
1208     # properties used when changing handle type
1209     bpy.context.window_manager.motion_trail.handle_type_frame = frame
1210     bpy.context.window_manager.motion_trail.handle_type_side = side
1211     bpy.context.window_manager.motion_trail.handle_type_action_ob = \
1212         action_ob.name
1213     if child:
1214         bpy.context.window_manager.motion_trail.handle_type_child = child.name
1215     else:
1216         bpy.context.window_manager.motion_trail.handle_type_child = ""
1217     
1218     curves = get_curves(action_ob, child=child)
1219     for c in curves:
1220         for kf in c.keyframe_points:
1221             if kf.co[0] == frame:
1222                 if side in ["left", "both"]:
1223                     return(kf.handle_left_type)
1224                 else:
1225                     return(kf.handle_right_type)
1226     
1227     return("AUTO")
1228
1229
1230 # turn the given frame into a keyframe
1231 def insert_keyframe(self, context, frame):
1232     objectname, frame, frame, action_ob, child = frame
1233     curves = get_curves(action_ob, child)
1234     for c in curves:
1235         y = c.evaluate(frame)
1236         if c.keyframe_points:
1237             c.keyframe_points.insert(frame, y)
1238     
1239     bpy.context.window_manager.motion_trail.force_update = True
1240     calc_callback(self, context)
1241
1242
1243 # change the handle type of the active selection
1244 def set_handle_type(self, context):
1245     if not context.window_manager.motion_trail.handle_type_enabled:
1246         return
1247     if context.window_manager.motion_trail.handle_type_old == \
1248     context.window_manager.motion_trail.handle_type:
1249         # function called because of selection change, not change in type
1250         return
1251     context.window_manager.motion_trail.handle_type_old = \
1252         context.window_manager.motion_trail.handle_type
1253     
1254     frame = bpy.context.window_manager.motion_trail.handle_type_frame
1255     side = bpy.context.window_manager.motion_trail.handle_type_side
1256     action_ob = bpy.context.window_manager.motion_trail.handle_type_action_ob
1257     action_ob = bpy.data.objects[action_ob]
1258     child = bpy.context.window_manager.motion_trail.handle_type_child
1259     if child:
1260         child = action_ob.pose.bones[child]
1261     new_type = context.window_manager.motion_trail.handle_type
1262     
1263     curves = get_curves(action_ob, child=child)
1264     for c in curves:
1265         for kf in c.keyframe_points:
1266             if kf.co[0] == frame:
1267                 # align if necessary
1268                 if side in ["right", "both"] and new_type in \
1269                 ["AUTO", "ALIGNED"]:
1270                     # change right handle
1271                     normal = (kf.co - kf.handle_left).normalized()
1272                     size = (kf.handle_right[0] - kf.co[0]) / normal[0]
1273                     normal = normal*size + kf.co
1274                     kf.handle_right[1] = normal[1]
1275                 elif side == "left" and new_type in ["AUTO", "ALIGNED"]:
1276                     # change left handle
1277                     normal = (kf.co - kf.handle_right).normalized()
1278                     size = (kf.handle_left[0] - kf.co[0]) / normal[0]
1279                     normal = normal*size + kf.co
1280                     kf.handle_left[1] = normal[1]
1281                 # change type
1282                 if side in ["left", "both"]:
1283                     kf.handle_left_type = new_type
1284                 if side in ["right", "both"]:
1285                     kf.handle_right_type = new_type
1286     
1287     context.window_manager.motion_trail.force_update = True
1288
1289
1290 class MotionTrailOperator(bpy.types.Operator):
1291     '''Edit motion trails in 3d-view'''
1292     bl_idname = "view3d.motion_trail"
1293     bl_label = "Motion Trail"
1294     
1295     _handle1 = None
1296     _handle2 = None
1297     
1298     def modal(self, context, event):
1299         if context.window_manager.motion_trail.enabled == -1:
1300             context.window_manager.motion_trail.enabled = 0
1301             try:
1302                 context.region.callback_remove(self._handle1)
1303             except:
1304                 pass
1305             try:
1306                 context.region.callback_remove(self._handle2)
1307             except:
1308                 pass
1309             context.area.tag_redraw()
1310             return {'FINISHED'}
1311         
1312         if not context.area:
1313             return {'PASS_THROUGH'}
1314         if not context.region or event.type == 'NONE':
1315             context.area.tag_redraw()
1316             return {'PASS_THROUGH'}
1317         
1318         select = context.user_preferences.inputs.select_mouse
1319         if not context.active_object or not context.active_object.mode in \
1320         ['OBJECT', 'POSE']:
1321             if self.drag:
1322                 self.drag = False
1323                 self.lock = True
1324                 context.window_manager.motion_trail.force_update = True
1325             # default hotkeys should still work
1326             if event.type == self.transform_key and event.value == 'PRESS':
1327                 if bpy.ops.transform.translate.poll():
1328                     bpy.ops.transform.translate('INVOKE_DEFAULT')
1329             elif event.type == select + 'MOUSE' and event.value == 'PRESS' \
1330             and not self.drag and not event.shift and not event.alt \
1331             and not event.ctrl:
1332                 if bpy.ops.view3d.select.poll():
1333                     bpy.ops.view3d.select('INVOKE_DEFAULT')
1334             elif event.type == 'LEFTMOUSE' and event.value == 'PRESS' and not\
1335             event.alt and not event.ctrl and not event.shift:
1336                 if eval("bpy.ops."+self.left_action+".poll()"):
1337                     eval("bpy.ops."+self.left_action+"('INVOKE_DEFAULT')")
1338             return {'PASS_THROUGH'}
1339         # check if event was generated within 3d-window, dragging is exception
1340         if not self.drag:
1341             if not (0 < event.mouse_region_x < context.region.width) or \
1342             not (0 < event.mouse_region_y < context.region.height):
1343                 return {'PASS_THROUGH'}
1344         
1345         if event.type == self.transform_key and event.value == 'PRESS' and \
1346         (self.active_keyframe or self.active_handle or self.active_timebead \
1347         or self.active_frame):
1348             # override default translate()
1349             if not self.drag:
1350                 # start drag
1351                 if self.active_frame:
1352                     insert_keyframe(self, context, self.active_frame)
1353                     self.active_keyframe = self.active_frame
1354                     self.active_frame = False
1355                 self.keyframes_ori, self.handles_ori = \
1356                     get_original_animation_data(context, self.keyframes)
1357                 self.drag_mouse_ori = mathutils.Vector([event.mouse_region_x,
1358                     event.mouse_region_y])
1359                 self.drag = True
1360                 self.lock = False
1361             else:
1362                 # stop drag
1363                 self.drag = False
1364                 self.lock = True
1365                 context.window_manager.motion_trail.force_update = True
1366         elif event.type == self.transform_key and event.value == 'PRESS':
1367             # call default translate()
1368             if bpy.ops.transform.translate.poll():
1369                 bpy.ops.transform.translate('INVOKE_DEFAULT')
1370         elif (event.type == 'ESC' and self.drag and event.value == 'PRESS') \
1371         or (event.type == 'RIGHTMOUSE' and self.drag and event.value == \
1372         'PRESS'):
1373             # cancel drag
1374             self.drag = False
1375             self.lock = True
1376             context.window_manager.motion_trail.force_update = True
1377             self.active_keyframe, self.active_timebead = cancel_drag(context,
1378                 self.active_keyframe, self.active_handle,
1379                 self.active_timebead, self.keyframes_ori, self.handles_ori,
1380                 self.edit_bones)
1381         elif event.type == 'MOUSEMOVE' and self.drag:
1382             # drag
1383             self.active_keyframe, self.active_timebead, self.keyframes_ori = \
1384                 drag(context, event, self.drag_mouse_ori,
1385                 self.active_keyframe, self.active_handle,
1386                 self.active_timebead, self.keyframes_ori, self.handles_ori,
1387                 self.edit_bones)
1388         elif event.type == select + 'MOUSE' and event.value == 'PRESS' and \
1389         not self.drag and not event.shift and not event.alt and not \
1390         event.ctrl:
1391             # select
1392             treshold = 10
1393             clicked = mathutils.Vector([event.mouse_region_x,
1394                 event.mouse_region_y])
1395             self.active_keyframe = False
1396             self.active_handle = False
1397             self.active_timebead = False
1398             self.active_frame = False
1399             context.window_manager.motion_trail.force_update = True
1400             context.window_manager.motion_trail.handle_type_enabled = True
1401             found = False
1402             
1403             if context.window_manager.motion_trail.path_before == 0:
1404                 frame_min = context.scene.frame_start
1405             else:
1406                 frame_min = max(context.scene.frame_start,
1407                     context.scene.frame_current - \
1408                     context.window_manager.motion_trail.path_before)
1409             if context.window_manager.motion_trail.path_after == 0:
1410                 frame_max = context.scene.frame_end
1411             else:
1412                 frame_max = min(context.scene.frame_end,
1413                     context.scene.frame_current + \
1414                     context.window_manager.motion_trail.path_after)
1415             
1416             for objectname, values in self.click.items():
1417                 if found:
1418                     break
1419                 for frame, type, coord, action_ob, child in values:
1420                     if frame < frame_min or frame > frame_max:
1421                         continue
1422                     if (coord - clicked).length <= treshold:
1423                         found = True
1424                         if type == "keyframe":
1425                             self.active_keyframe = [objectname, frame, frame,
1426                                 action_ob, child]
1427                         elif type == "handle_left":
1428                             self.active_handle = [objectname, frame, "left",
1429                                 action_ob, child]
1430                         elif type == "handle_right":
1431                             self.active_handle = [objectname, frame, "right",
1432                                 action_ob, child]
1433                         elif type == "timebead":
1434                             self.active_timebead = [objectname, frame, frame,
1435                                 action_ob, child]
1436                         elif type == "frame":
1437                             self.active_frame = [objectname, frame, frame,
1438                                 action_ob, child]
1439                         break
1440             if not found:
1441                 context.window_manager.motion_trail.handle_type_enabled = False
1442                 # no motion trail selections, so pass on to normal select()
1443                 if bpy.ops.view3d.select.poll():
1444                     bpy.ops.view3d.select('INVOKE_DEFAULT')
1445             else:
1446                 handle_type = get_handle_type(self.active_keyframe,
1447                     self.active_handle)
1448                 if handle_type:
1449                     context.window_manager.motion_trail.handle_type_old = \
1450                         handle_type
1451                     context.window_manager.motion_trail.handle_type = \
1452                         handle_type
1453                 else:
1454                     context.window_manager.motion_trail.handle_type_enabled = \
1455                         False
1456         elif event.type == 'LEFTMOUSE' and event.value == 'PRESS' and \
1457         self.drag:
1458             # stop drag
1459             self.drag = False
1460             self.lock = True
1461             context.window_manager.motion_trail.force_update = True
1462         elif event.type == 'LEFTMOUSE' and event.value == 'PRESS' and not\
1463         event.alt and not event.ctrl and not event.shift:
1464             if eval("bpy.ops."+self.left_action+".poll()"):
1465                 eval("bpy.ops."+self.left_action+"('INVOKE_DEFAULT')")
1466         
1467         if context.area: # not available if other window-type is fullscreen
1468             context.area.tag_redraw()
1469         
1470         return {'PASS_THROUGH'}
1471
1472     def invoke(self, context, event):
1473         if context.area.type == 'VIEW_3D':
1474             # get clashing keymap items
1475             select = context.user_preferences.inputs.select_mouse
1476             kms = [bpy.context.window_manager.keyconfigs.active.\
1477                 keymaps['3D View'], bpy.context.window_manager.keyconfigs.\
1478                 active.keymaps['Object Mode']]
1479             kmis = []
1480             self.left_action = None
1481             self.right_action = None
1482             for km in kms:
1483                 for kmi in km.keymap_items:
1484                     if kmi.idname == "transform.translate" and \
1485                     kmi.map_type == 'KEYBOARD' and not \
1486                     kmi.properties.texture_space:
1487                         kmis.append(kmi)
1488                         self.transform_key = kmi.type
1489                     elif (kmi.type == 'ACTIONMOUSE' and select == 'RIGHT') \
1490                     and not kmi.alt and not kmi.any and not kmi.ctrl \
1491                     and not kmi.shift:
1492                         kmis.append(kmi)
1493                         self.left_action = kmi.idname
1494                     elif kmi.type == 'SELECTMOUSE' and not kmi.alt and not \
1495                     kmi.any and not kmi.ctrl and not kmi.shift:
1496                         kmis.append(kmi)
1497                         if select == 'RIGHT':
1498                             self.right_action = kmi.idname
1499                         else:
1500                             self.left_action = kmi.idname
1501                     elif kmi.type == 'LEFTMOUSE' and not kmi.alt and not \
1502                     kmi.any and not kmi.ctrl and not kmi.shift:
1503                         kmis.append(kmi)
1504                         self.left_action = kmi.idname
1505             
1506             if context.window_manager.motion_trail.enabled == 0:
1507                 # enable
1508                 context.window_manager.motion_trail.enabled = 1
1509                 self.active_keyframe = False
1510                 self.active_handle = False
1511                 self.active_timebead = False
1512                 self.active_frame = False
1513                 self.click = {}
1514                 self.drag = False
1515                 self.lock = True
1516                 self.perspective = context.region_data.perspective_matrix
1517                 self.displayed = []
1518                 context.window_manager.motion_trail.force_update = True
1519                 context.window_manager.motion_trail.handle_type_enabled = False
1520                 self.cached = {"path":{}, "keyframes":{},
1521                     "timebeads_timing":{}, "timebeads_speed":{}}
1522                 
1523                 for kmi in kmis:
1524                     kmi.active = False
1525                 
1526                 context.window_manager.modal_handler_add(self)
1527                 self._handle1 = context.region.callback_add(calc_callback,
1528                     (self, context), 'POST_VIEW')
1529                 self._handle2 = context.region.callback_add(draw_callback,
1530                     (self, context), 'POST_PIXEL')
1531                 if context.area:
1532                     context.area.tag_redraw()
1533             else:
1534                 # disable
1535                 context.window_manager.motion_trail.enabled = -1
1536                 for kmi in kmis:
1537                     kmi.active = True
1538             
1539             return {'RUNNING_MODAL'}
1540         
1541         else:
1542             self.report({'WARNING'}, "View3D not found, cannot run operator")
1543             return {'CANCELLED'}
1544
1545
1546 class MotionTrailPanel(bpy.types.Panel):
1547     bl_space_type = 'VIEW_3D'
1548     bl_region_type = 'TOOLS'
1549     bl_label = "Motion Trail"
1550
1551     @classmethod
1552     def poll(cls, context):
1553         if not context.active_object:
1554             return(False)
1555         return(context.active_object.mode in ['OBJECT', 'POSE'])
1556     
1557     def draw(self, context):
1558         col = self.layout.column()
1559         if context.window_manager.motion_trail.enabled != 1:
1560             col.operator("view3d.motion_trail", text="Enable motion trail")
1561         else:
1562             col.operator("view3d.motion_trail", text="Disable motion trail")
1563         
1564         box = self.layout.box()
1565         box.prop(context.window_manager.motion_trail, "mode")
1566         #box.prop(context.window_manager.motion_trail, "calculate")
1567         if context.window_manager.motion_trail.mode in ['timing']:
1568             box.prop(context.window_manager.motion_trail, "timebeads")
1569         
1570         box = self.layout.box()
1571         col = box.column()
1572         row = col.row()
1573         if context.window_manager.motion_trail.path_display:
1574             row.prop(context.window_manager.motion_trail, "path_display",
1575                 icon="DOWNARROW_HLT", text="", emboss=False)
1576         else:
1577             row.prop(context.window_manager.motion_trail, "path_display",
1578                 icon="RIGHTARROW", text="", emboss=False)
1579         row.label("Path options")
1580         if context.window_manager.motion_trail.path_display:
1581             col.prop(context.window_manager.motion_trail, "path_style",
1582                 text="Style")
1583             grouped = col.column(align=True)
1584             grouped.prop(context.window_manager.motion_trail, "path_width",
1585                 text="Width")
1586             grouped.prop(context.window_manager.motion_trail,
1587                 "path_transparency", text="Transparency")
1588             grouped.prop(context.window_manager.motion_trail,
1589                 "path_resolution")
1590             row = grouped.row(align=True)
1591             row.prop(context.window_manager.motion_trail, "path_before")
1592             row.prop(context.window_manager.motion_trail, "path_after")
1593             col = col.column(align=True)
1594             col.prop(context.window_manager.motion_trail, "keyframe_numbers")
1595             col.prop(context.window_manager.motion_trail, "frame_display")
1596         
1597         if context.window_manager.motion_trail.mode in ['location']:
1598             box = self.layout.box()
1599             col = box.column(align=True)
1600             col.prop(context.window_manager.motion_trail, "handle_display",
1601                 text="Handles")
1602             if context.window_manager.motion_trail.handle_display:
1603                 row = col.row()
1604                 row.enabled = context.window_manager.motion_trail.\
1605                     handle_type_enabled
1606                 row.prop(context.window_manager.motion_trail, "handle_type")
1607
1608
1609 class MotionTrailProps(bpy.types.PropertyGroup):
1610     def internal_update(self, context):
1611         context.window_manager.motion_trail.force_update = True
1612         if context.area:
1613             context.area.tag_redraw()
1614     
1615     # internal use
1616     enabled = bpy.props.IntProperty(default=0)
1617     force_update = bpy.props.BoolProperty(name="internal use",
1618         description="Force calc_callback to fully execute",
1619         default=False)
1620     handle_type_enabled = bpy.props.BoolProperty(default=False)
1621     handle_type_frame = bpy.props.FloatProperty()
1622     handle_type_side = bpy.props.StringProperty()
1623     handle_type_action_ob = bpy.props.StringProperty()
1624     handle_type_child = bpy.props.StringProperty()
1625     handle_type_old = bpy.props.EnumProperty(items=(("AUTO", "", ""), 
1626         ("VECTOR", "", ""), ("ALIGNED", "", ""), ("FREE", "", "")),
1627         default='AUTO',)
1628     
1629     # visible in user interface
1630     calculate = bpy.props.EnumProperty(name="Calculate",
1631         items=(("fast", "Fast", "Recommended setting, change if the "\
1632             "motion path is positioned incorrectly"),
1633             ("full", "Full", "Takes parenting and modifiers into account, "\
1634             "but can be very slow on complicated scenes")),
1635         description="Calculation method for determining locations",
1636         default='full',
1637         update=internal_update)
1638     frame_display = bpy.props.BoolProperty(name="Frames",
1639         description="Display frames, \n test",
1640         default=True,
1641         update=internal_update)
1642     handle_display = bpy.props.BoolProperty(name="Display",
1643         description="Display handles",
1644         default=True,
1645         update=internal_update)
1646     handle_type = bpy.props.EnumProperty(name="Type",
1647         items=(("AUTO", "Automatic", ""),
1648             ("VECTOR", "Vector", ""),
1649             ("ALIGNED", "Aligned", ""),
1650             ("FREE", "Free", "")),
1651         description="Set handle type for the selected handle",
1652         default='AUTO',
1653         update=set_handle_type)
1654     keyframe_numbers = bpy.props.BoolProperty(name="Keyframe numbers",
1655         description="Display keyframe numbers",
1656         default=False,
1657         update=internal_update)
1658     mode = bpy.props.EnumProperty(name="Mode",
1659         items=(("location", "Location", "Change path that is followed"),
1660             ("speed", "Speed", "Change speed between keyframes"),
1661             ("timing", "Timing", "Change position of keyframes on timeline")),
1662         description="Enable editing of certain properties in the 3d-view",
1663         default='location',
1664         update=internal_update)
1665     path_after = bpy.props.IntProperty(name="After",
1666         description="Number of frames to show after the current frame, "\
1667             "0 = display all",
1668         default=50,
1669         min=0,
1670         update=internal_update)
1671     path_before = bpy.props.IntProperty(name="Before",
1672         description="Number of frames to show before the current frame, "\
1673             "0 = display all",
1674         default=50,
1675         min=0,
1676         update=internal_update)
1677     path_display = bpy.props.BoolProperty(name="Path options",
1678         description="Display path options",
1679         default=True)
1680     path_resolution = bpy.props.IntProperty(name="Resolution",
1681         description="10 is smoothest, but could be "\
1682         "slow when adjusting keyframes, handles or timebeads",
1683         default=10,
1684         min=1,
1685         max=10,
1686         update=internal_update)
1687     path_style = bpy.props.EnumProperty(name="Path style",
1688         items=(("acceleration", "Acceleration", "Gradient based on relative "\
1689                 "acceleration"),
1690             ("simple", "Simple", "Black line"),
1691             ("speed", "Speed", "Gradient based on relative speed")),
1692         description="Information conveyed by path colour",
1693         default='simple',
1694         update=internal_update)
1695     path_transparency = bpy.props.IntProperty(name="Path transparency",
1696         description="Determines visibility of path",
1697         default=0,
1698         min=0,
1699         max=100,
1700         subtype='PERCENTAGE',
1701         update=internal_update)
1702     path_width = bpy.props.IntProperty(name="Path width",
1703         description="Width in pixels",
1704         default=1,
1705         min=1,
1706         soft_max=5,
1707         update=internal_update)
1708     timebeads = bpy.props.IntProperty(name="Time beads",
1709         description="Number of time beads to display per segment",
1710         default=5,
1711         min=1,
1712         soft_max = 10,
1713         update=internal_update)
1714
1715
1716 classes = [MotionTrailProps,
1717     MotionTrailOperator,
1718     MotionTrailPanel]
1719
1720
1721 def register():
1722     for c in classes:
1723         bpy.utils.register_class(c)
1724     bpy.types.WindowManager.motion_trail = bpy.props.PointerProperty(\
1725         type=MotionTrailProps)
1726
1727
1728 def unregister():
1729     for c in classes:
1730         bpy.utils.unregister_class(c)
1731     del bpy.types.WindowManager.motion_trail
1732
1733
1734 if __name__ == "__main__":
1735     register()