fix [#33615] bl_info (2,6,5,0) vs. (2,65,0) ?
[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, 1),
26     "blender": (2, 61, 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.6/"
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, color
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 color 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, color, 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, color, 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, color, frame, action_ob, child] in enumerate(path):
620                 if frame < limit_min or frame > limit_max:
621                     continue
622                 r, g, b = color
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, color, 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', 'AUTO_CLAMPED',
852                         'ANIM_CLAMPED']:
853                             kf.handle_left_type = 'ALIGNED'
854                         elif kf.handle_left_type == 'VECTOR':
855                             kf.handle_left_type = 'FREE'
856                         # change handle position(s)
857                         kf.handle_left[1] = handles_ori[objectname][frame]\
858                             ["left"][i][1] + d[i]
859                         if kf.handle_left_type in ['ALIGNED', 'ANIM_CLAMPED',
860                         'AUTO', 'AUTO_CLAMPED']:
861                             dif = (abs(handles_ori[objectname][frame]["right"]\
862                                 [i][0] - kf.co[0]) / abs(kf.handle_left[0] - \
863                                 kf.co[0])) * d[i]
864                             kf.handle_right[1] = handles_ori[objectname]\
865                                 [frame]["right"][i][1] - dif
866                     elif side == "right":
867                         # change handle type, if necessary
868                         if kf.handle_right_type in ['AUTO', 'AUTO_CLAMPED',
869                         'ANIM_CLAMPED']:
870                             kf.handle_left_type = 'ALIGNED'
871                             kf.handle_right_type = 'ALIGNED'
872                         elif kf.handle_right_type == 'VECTOR':
873                             kf.handle_left_type = 'FREE'
874                             kf.handle_right_type = 'FREE'
875                         # change handle position(s)
876                         kf.handle_right[1] = handles_ori[objectname][frame]\
877                             ["right"][i][1] + d[i]
878                         if kf.handle_right_type in ['ALIGNED', 'ANIM_CLAMPED',
879                         'AUTO', 'AUTO_CLAMPED']:
880                             dif = (abs(handles_ori[objectname][frame]["left"]\
881                                 [i][0] - kf.co[0]) / abs(kf.handle_right[0] - \
882                                 kf.co[0])) * d[i]
883                             kf.handle_left[1] = handles_ori[objectname]\
884                                 [frame]["left"][i][1] - dif
885                     break
886     
887     # change position of all keyframes on timeline
888     elif context.window_manager.motion_trail.mode == 'timing' and \
889     active_timebead:
890         objectname, frame, frame_ori, action_ob, child = active_timebead
891         curves = get_curves(action_ob, child)
892         ranges = [val for c in curves for val in c.range()]
893         ranges.sort()
894         range_min = round(ranges[0])
895         range_max = round(ranges[-1])
896         range = range_max - range_min
897         dx_screen = -(mathutils.Vector([event.mouse_region_x,
898             event.mouse_region_y]) - drag_mouse_ori)[0]
899         dx_screen = dx_screen / context.region.width * range
900         new_frame = frame + dx_screen
901         shift_low = max(1e-4, (new_frame - range_min) / (frame - range_min))
902         shift_high = max(1e-4, (range_max - new_frame) / (range_max - frame))
903         
904         new_mapping = {}
905         for i, curve in enumerate(curves):
906             for j, kf in enumerate(curve.keyframe_points):
907                 frame_map = kf.co[0]
908                 if frame_map < range_min + 1e-4 or \
909                 frame_map > range_max - 1e-4:
910                     continue
911                 frame_ori = False
912                 for f in keyframes_ori[objectname]:
913                     if abs(f - frame_map) < 1e-4:
914                         frame_ori = keyframes_ori[objectname][f][0]
915                         value_ori = keyframes_ori[objectname][f]
916                         break
917                 if not frame_ori:
918                     continue
919                 if frame_ori <= frame:
920                     frame_new = (frame_ori - range_min) * shift_low + \
921                         range_min
922                 else:
923                     frame_new = range_max - (range_max - frame_ori) * \
924                         shift_high
925                 frame_new = max(range_min + j, min(frame_new, range_max - \
926                     (len(curve.keyframe_points)-j)+1))
927                 d_frame = frame_new - frame_ori
928                 if frame_new not in new_mapping:
929                     new_mapping[frame_new] = value_ori
930                 kf.co[0] = frame_new
931                 kf.handle_left[0] = handles_ori[objectname][frame_ori]\
932                     ["left"][i][0] + d_frame
933                 kf.handle_right[0] = handles_ori[objectname][frame_ori]\
934                     ["right"][i][0] + d_frame
935         del keyframes_ori[objectname]
936         keyframes_ori[objectname] = {}
937         for new_frame, value in new_mapping.items():
938             keyframes_ori[objectname][new_frame] = value
939     
940     # change position of active keyframe on the timeline
941     elif context.window_manager.motion_trail.mode == 'timing' and \
942     active_keyframe:
943         objectname, frame, frame_ori, action_ob, child = active_keyframe
944         if child:
945             mat = action_ob.matrix_world.copy().inverted() * \
946                 edit_bones[child.name].copy().to_4x4()
947         else:
948             mat = action_ob.matrix_world.copy().inverted()
949         
950         mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
951             drag_mouse_ori[1]) * mat
952         vector = screen_to_world(context, event.mouse_region_x,
953             event.mouse_region_y) * mat
954         d = vector - mouse_ori_world
955         
956         locs_ori = [[f_ori, coords] for f_mapped, [f_ori, coords] in \
957             keyframes_ori[objectname].items()]
958         locs_ori.sort()
959         direction = 1
960         range = False
961         for i, [f_ori, coords] in enumerate(locs_ori):
962             if abs(frame_ori - f_ori) < 1e-4:
963                 if i == 0:
964                     # first keyframe, nothing before it
965                     direction = -1
966                     range = [f_ori, locs_ori[i+1][0]]
967                 elif i == len(locs_ori) - 1:
968                     # last keyframe, nothing after it
969                     range = [locs_ori[i-1][0], f_ori]
970                 else:
971                     current = mathutils.Vector(coords)
972                     next = mathutils.Vector(locs_ori[i+1][1])
973                     previous = mathutils.Vector(locs_ori[i-1][1])
974                     angle_to_next = d.angle(next - current, 0)
975                     angle_to_previous = d.angle(previous-current, 0)
976                     if angle_to_previous < angle_to_next:
977                         # mouse movement is in direction of previous keyframe
978                         direction = -1
979                     range = [locs_ori[i-1][0], locs_ori[i+1][0]]
980                 break
981         direction *= -1 # feels more natural in 3d-view
982         if not range:
983             # keyframe not found, is impossible, but better safe than sorry
984             return(active_keyframe, active_timebead, keyframes_ori)
985         # calculate strength of movement
986         d_screen = mathutils.Vector([event.mouse_region_x,
987             event.mouse_region_y]) - drag_mouse_ori
988         if d_screen.length != 0:
989             d_screen = d_screen.length / (abs(d_screen[0])/d_screen.length*\
990                 context.region.width + abs(d_screen[1])/d_screen.length*\
991                 context.region.height)
992             d_screen *= direction  # d_screen value ranges from -1.0 to 1.0
993         else:
994             d_screen = 0.0
995         new_frame = d_screen * (range[1] - range[0]) + frame_ori
996         max_frame = range[1]
997         if max_frame == frame_ori:
998             max_frame += 1
999         min_frame = range[0]
1000         if min_frame == frame_ori:
1001             min_frame -= 1
1002         new_frame = min(max_frame - 1, max(min_frame + 1, new_frame))
1003         d_frame = new_frame - frame_ori
1004         curves = get_curves(action_ob, child)
1005         
1006         for i, curve in enumerate(curves):
1007             for kf in curve.keyframe_points:
1008                 if abs(kf.co[0] - frame) < 1e-4:
1009                     kf.co[0] = new_frame
1010                     kf.handle_left[0] = handles_ori[objectname][frame_ori]\
1011                         ["left"][i][0] + d_frame
1012                     kf.handle_right[0] = handles_ori[objectname][frame_ori]\
1013                         ["right"][i][0] + d_frame
1014                     break
1015         active_keyframe = [objectname, new_frame, frame_ori, action_ob, child]
1016     
1017     # change position of active timebead on the timeline, thus altering speed
1018     elif context.window_manager.motion_trail.mode == 'speed' and \
1019     active_timebead:
1020         objectname, frame, frame_ori, action_ob, child = active_timebead
1021         if child:
1022             mat = action_ob.matrix_world.copy().inverted() * \
1023                 edit_bones[child.name].copy().to_4x4()
1024         else:
1025             mat = 1
1026         
1027         mouse_ori_world = screen_to_world(context, drag_mouse_ori[0],
1028             drag_mouse_ori[1]) * mat
1029         vector = screen_to_world(context, event.mouse_region_x,
1030             event.mouse_region_y) * mat
1031         d = vector - mouse_ori_world
1032         
1033         # determine direction (to next or previous keyframe)
1034         curves = get_curves(action_ob, child)
1035         fcx, fcy, fcz = curves
1036         locx = fcx.evaluate(frame_ori)
1037         locy = fcy.evaluate(frame_ori)
1038         locz = fcz.evaluate(frame_ori)
1039         loc_ori = mathutils.Vector([locx, locy, locz]) # bonespace
1040         keyframes = [kf for kf in keyframes_ori[objectname]]
1041         keyframes.append(frame_ori)
1042         keyframes.sort()
1043         frame_index = keyframes.index(frame_ori)
1044         kf_prev = keyframes[frame_index - 1]
1045         kf_next = keyframes[frame_index + 1]
1046         vec_prev = (mathutils.Vector(keyframes_ori[objectname][kf_prev][1]) \
1047             * mat - loc_ori).normalized()
1048         vec_next = (mathutils.Vector(keyframes_ori[objectname][kf_next][1]) \
1049             * mat - loc_ori).normalized()
1050         d_normal = d.copy().normalized()
1051         dist_to_next = (d_normal - vec_next).length
1052         dist_to_prev = (d_normal - vec_prev).length
1053         if dist_to_prev < dist_to_next:
1054             direction = 1
1055         else:
1056             direction = -1
1057         
1058         if (kf_next - frame_ori) < (frame_ori - kf_prev):
1059             kf_bead = kf_next
1060             side = "left"
1061         else:
1062             kf_bead = kf_prev
1063             side = "right"
1064         d_frame = d.length * direction * 2 # *2 to make it more sensitive
1065         
1066         angles = []
1067         for i, curve in enumerate(curves):
1068             for kf in curve.keyframe_points:
1069                 if abs(kf.co[0] - kf_bead) < 1e-4:
1070                     if side == "left":
1071                         # left side
1072                         kf.handle_left[0] = min(handles_ori[objectname]\
1073                             [kf_bead]["left"][i][0] + d_frame, kf_bead - 1)
1074                         angle = mathutils.Vector([-1, 0]).angle(mathutils.\
1075                             Vector(kf.handle_left) - mathutils.Vector(kf.co),
1076                             0)
1077                         if angle != 0:
1078                             angles.append(angle)
1079                     else:
1080                         # right side
1081                         kf.handle_right[0] = max(handles_ori[objectname]\
1082                             [kf_bead]["right"][i][0] + d_frame, kf_bead + 1)
1083                         angle = mathutils.Vector([1, 0]).angle(mathutils.\
1084                             Vector(kf.handle_right) - mathutils.Vector(kf.co),
1085                             0)
1086                         if angle != 0:
1087                             angles.append(angle)
1088                     break
1089         
1090         # update frame of active_timebead
1091         perc = (sum(angles) / len(angles)) / (math.pi / 2)
1092         perc = max(0.4, min(1, perc * 5))
1093         if side == "left":
1094             bead_frame = kf_bead - perc * ((kf_bead - kf_prev - 2) / 2)
1095         else:
1096             bead_frame = kf_bead + perc * ((kf_next - kf_bead - 2) / 2)
1097         active_timebead = [objectname, bead_frame, frame_ori, action_ob, child]
1098     
1099     return(active_keyframe, active_timebead, keyframes_ori)
1100
1101
1102 # revert changes made by dragging
1103 def cancel_drag(context, active_keyframe, active_handle, active_timebead,
1104 keyframes_ori, handles_ori, edit_bones):
1105     # revert change in 3d-location of active keyframe and its handles
1106     if context.window_manager.motion_trail.mode == 'location' and \
1107     active_keyframe:
1108         objectname, frame, frame_ori, active_ob, child = active_keyframe
1109         curves = get_curves(active_ob, child)
1110         loc_ori = keyframes_ori[objectname][frame][1]
1111         if child:
1112             loc_ori = loc_ori * edit_bones[child.name] * \
1113                 active_ob.matrix_world.copy().inverted()
1114         for i, curve in enumerate(curves):
1115             for kf in curve.keyframe_points:
1116                 if kf.co[0] == frame:
1117                     kf.co[1] = loc_ori[i]
1118                     kf.handle_left[1] = handles_ori[objectname][frame]\
1119                         ["left"][i][1]
1120                     kf.handle_right[1] = handles_ori[objectname][frame]\
1121                         ["right"][i][1]
1122                     break
1123     
1124     # revert change in 3d-location of active handle
1125     elif context.window_manager.motion_trail.mode == 'location' and \
1126     active_handle:
1127         objectname, frame, side, active_ob, child = active_handle
1128         curves = get_curves(active_ob, child)
1129         for i, curve in enumerate(curves):
1130             for kf in curve.keyframe_points:
1131                 if kf.co[0] == frame:
1132                     kf.handle_left[1] = handles_ori[objectname][frame]\
1133                         ["left"][i][1]
1134                     kf.handle_right[1] = handles_ori[objectname][frame]\
1135                         ["right"][i][1]
1136                     break
1137     
1138     # revert position of all keyframes and handles on timeline
1139     elif context.window_manager.motion_trail.mode == 'timing' and \
1140     active_timebead:
1141         objectname, frame, frame_ori, active_ob, child = active_timebead
1142         curves = get_curves(active_ob, child)
1143         for i, curve in enumerate(curves):
1144             for kf in curve.keyframe_points:
1145                 for kf_ori, [frame_ori, loc] in keyframes_ori[objectname].\
1146                 items():
1147                     if abs(kf.co[0] - kf_ori) < 1e-4:
1148                         kf.co[0] = frame_ori
1149                         kf.handle_left[0] = handles_ori[objectname]\
1150                             [frame_ori]["left"][i][0]
1151                         kf.handle_right[0] = handles_ori[objectname]\
1152                             [frame_ori]["right"][i][0]
1153                         break
1154     
1155     # revert position of active keyframe and its handles on the timeline
1156     elif context.window_manager.motion_trail.mode == 'timing' and \
1157     active_keyframe:
1158         objectname, frame, frame_ori, active_ob, child = active_keyframe
1159         curves = get_curves(active_ob, child)
1160         for i, curve in enumerate(curves):
1161             for kf in curve.keyframe_points:
1162                 if abs(kf.co[0] - frame) < 1e-4:
1163                     kf.co[0] = keyframes_ori[objectname][frame_ori][0]
1164                     kf.handle_left[0] = handles_ori[objectname][frame_ori]\
1165                         ["left"][i][0]
1166                     kf.handle_right[0] = handles_ori[objectname][frame_ori]\
1167                         ["right"][i][0]
1168                     break
1169         active_keyframe = [objectname, frame_ori, frame_ori, active_ob, child]
1170     
1171     # revert position of handles on the timeline
1172     elif context.window_manager.motion_trail.mode == 'speed' and \
1173     active_timebead:
1174         objectname, frame, frame_ori, active_ob, child = active_timebead
1175         curves = get_curves(active_ob, child)
1176         keyframes = [kf for kf in keyframes_ori[objectname]]
1177         keyframes.append(frame_ori)
1178         keyframes.sort()
1179         frame_index = keyframes.index(frame_ori)
1180         kf_prev = keyframes[frame_index - 1]
1181         kf_next = keyframes[frame_index + 1]
1182         if (kf_next - frame_ori) < (frame_ori - kf_prev):
1183             kf_frame = kf_next
1184         else:
1185             kf_frame = kf_prev
1186         for i, curve in enumerate(curves):
1187             for kf in curve.keyframe_points:
1188                 if kf.co[0] == kf_frame:
1189                     kf.handle_left[0] = handles_ori[objectname][kf_frame]\
1190                         ["left"][i][0]
1191                     kf.handle_right[0] = handles_ori[objectname][kf_frame]\
1192                         ["right"][i][0]
1193                     break
1194         active_timebead = [objectname, frame_ori, frame_ori, active_ob, child]
1195     
1196     return(active_keyframe, active_timebead)
1197
1198
1199 # return the handle type of the active selection
1200 def get_handle_type(active_keyframe, active_handle):
1201     if active_keyframe:
1202         objectname, frame, side, action_ob, child = active_keyframe
1203         side = "both"
1204     elif active_handle:
1205         objectname, frame, side, action_ob, child = active_handle
1206     else:
1207         # no active handle(s)
1208         return(False)
1209     
1210     # properties used when changing handle type
1211     bpy.context.window_manager.motion_trail.handle_type_frame = frame
1212     bpy.context.window_manager.motion_trail.handle_type_side = side
1213     bpy.context.window_manager.motion_trail.handle_type_action_ob = \
1214         action_ob.name
1215     if child:
1216         bpy.context.window_manager.motion_trail.handle_type_child = child.name
1217     else:
1218         bpy.context.window_manager.motion_trail.handle_type_child = ""
1219     
1220     curves = get_curves(action_ob, child=child)
1221     for c in curves:
1222         for kf in c.keyframe_points:
1223             if kf.co[0] == frame:
1224                 if side in ["left", "both"]:
1225                     return(kf.handle_left_type)
1226                 else:
1227                     return(kf.handle_right_type)
1228     
1229     return("AUTO")
1230
1231
1232 # turn the given frame into a keyframe
1233 def insert_keyframe(self, context, frame):
1234     objectname, frame, frame, action_ob, child = frame
1235     curves = get_curves(action_ob, child)
1236     for c in curves:
1237         y = c.evaluate(frame)
1238         if c.keyframe_points:
1239             c.keyframe_points.insert(frame, y)
1240     
1241     bpy.context.window_manager.motion_trail.force_update = True
1242     calc_callback(self, context)
1243
1244
1245 # change the handle type of the active selection
1246 def set_handle_type(self, context):
1247     if not context.window_manager.motion_trail.handle_type_enabled:
1248         return
1249     if context.window_manager.motion_trail.handle_type_old == \
1250     context.window_manager.motion_trail.handle_type:
1251         # function called because of selection change, not change in type
1252         return
1253     context.window_manager.motion_trail.handle_type_old = \
1254         context.window_manager.motion_trail.handle_type
1255     
1256     frame = bpy.context.window_manager.motion_trail.handle_type_frame
1257     side = bpy.context.window_manager.motion_trail.handle_type_side
1258     action_ob = bpy.context.window_manager.motion_trail.handle_type_action_ob
1259     action_ob = bpy.data.objects[action_ob]
1260     child = bpy.context.window_manager.motion_trail.handle_type_child
1261     if child:
1262         child = action_ob.pose.bones[child]
1263     new_type = context.window_manager.motion_trail.handle_type
1264     
1265     curves = get_curves(action_ob, child=child)
1266     for c in curves:
1267         for kf in c.keyframe_points:
1268             if kf.co[0] == frame:
1269                 # align if necessary
1270                 if side in ["right", "both"] and new_type in \
1271                 ["AUTO", "AUTO_CLAMPED", "ALIGNED"]:
1272                     # change right handle
1273                     normal = (kf.co - kf.handle_left).normalized()
1274                     size = (kf.handle_right[0] - kf.co[0]) / normal[0]
1275                     normal = normal*size + kf.co
1276                     kf.handle_right[1] = normal[1]
1277                 elif side == "left" and new_type in ["AUTO", "AUTO_CLAMPED",
1278                 "ALIGNED"]:
1279                     # change left handle
1280                     normal = (kf.co - kf.handle_right).normalized()
1281                     size = (kf.handle_left[0] - kf.co[0]) / normal[0]
1282                     normal = normal*size + kf.co
1283                     kf.handle_left[1] = normal[1]
1284                 # change type
1285                 if side in ["left", "both"]:
1286                     kf.handle_left_type = new_type
1287                 if side in ["right", "both"]:
1288                     kf.handle_right_type = new_type
1289     
1290     context.window_manager.motion_trail.force_update = True
1291
1292
1293 class MotionTrailOperator(bpy.types.Operator):
1294     """Edit motion trails in 3d-view"""
1295     bl_idname = "view3d.motion_trail"
1296     bl_label = "Motion Trail"
1297     
1298     _handle1 = None
1299     _handle2 = None
1300     
1301     def modal(self, context, event):
1302         if context.window_manager.motion_trail.enabled == -1:
1303             context.window_manager.motion_trail.enabled = 0
1304             try:
1305                 context.region.callback_remove(self._handle1)
1306             except:
1307                 pass
1308             try:
1309                 context.region.callback_remove(self._handle2)
1310             except:
1311                 pass
1312             context.area.tag_redraw()
1313             return {'FINISHED'}
1314         
1315         if not context.area:
1316             return {'PASS_THROUGH'}
1317         if not context.region or event.type == 'NONE':
1318             context.area.tag_redraw()
1319             return {'PASS_THROUGH'}
1320         
1321         select = context.user_preferences.inputs.select_mouse
1322         if not context.active_object or not context.active_object.mode in \
1323         ['OBJECT', 'POSE']:
1324             if self.drag:
1325                 self.drag = False
1326                 self.lock = True
1327                 context.window_manager.motion_trail.force_update = True
1328             # default hotkeys should still work
1329             if event.type == self.transform_key and event.value == 'PRESS':
1330                 if bpy.ops.transform.translate.poll():
1331                     bpy.ops.transform.translate('INVOKE_DEFAULT')
1332             elif event.type == select + 'MOUSE' and event.value == 'PRESS' \
1333             and not self.drag and not event.shift and not event.alt \
1334             and not event.ctrl:
1335                 if bpy.ops.view3d.select.poll():
1336                     bpy.ops.view3d.select('INVOKE_DEFAULT')
1337             elif event.type == 'LEFTMOUSE' and event.value == 'PRESS' and not\
1338             event.alt and not event.ctrl and not event.shift:
1339                 if eval("bpy.ops."+self.left_action+".poll()"):
1340                     eval("bpy.ops."+self.left_action+"('INVOKE_DEFAULT')")
1341             return {'PASS_THROUGH'}
1342         # check if event was generated within 3d-window, dragging is exception
1343         if not self.drag:
1344             if not (0 < event.mouse_region_x < context.region.width) or \
1345             not (0 < event.mouse_region_y < context.region.height):
1346                 return {'PASS_THROUGH'}
1347         
1348         if event.type == self.transform_key and event.value == 'PRESS' and \
1349         (self.active_keyframe or self.active_handle or self.active_timebead \
1350         or self.active_frame):
1351             # override default translate()
1352             if not self.drag:
1353                 # start drag
1354                 if self.active_frame:
1355                     insert_keyframe(self, context, self.active_frame)
1356                     self.active_keyframe = self.active_frame
1357                     self.active_frame = False
1358                 self.keyframes_ori, self.handles_ori = \
1359                     get_original_animation_data(context, self.keyframes)
1360                 self.drag_mouse_ori = mathutils.Vector([event.mouse_region_x,
1361                     event.mouse_region_y])
1362                 self.drag = True
1363                 self.lock = False
1364             else:
1365                 # stop drag
1366                 self.drag = False
1367                 self.lock = True
1368                 context.window_manager.motion_trail.force_update = True
1369         elif event.type == self.transform_key and event.value == 'PRESS':
1370             # call default translate()
1371             if bpy.ops.transform.translate.poll():
1372                 bpy.ops.transform.translate('INVOKE_DEFAULT')
1373         elif (event.type == 'ESC' and self.drag and event.value == 'PRESS') \
1374         or (event.type == 'RIGHTMOUSE' and self.drag and event.value == \
1375         'PRESS'):
1376             # cancel drag
1377             self.drag = False
1378             self.lock = True
1379             context.window_manager.motion_trail.force_update = True
1380             self.active_keyframe, self.active_timebead = cancel_drag(context,
1381                 self.active_keyframe, self.active_handle,
1382                 self.active_timebead, self.keyframes_ori, self.handles_ori,
1383                 self.edit_bones)
1384         elif event.type == 'MOUSEMOVE' and self.drag:
1385             # drag
1386             self.active_keyframe, self.active_timebead, self.keyframes_ori = \
1387                 drag(context, event, self.drag_mouse_ori,
1388                 self.active_keyframe, self.active_handle,
1389                 self.active_timebead, self.keyframes_ori, self.handles_ori,
1390                 self.edit_bones)
1391         elif event.type == select + 'MOUSE' and event.value == 'PRESS' and \
1392         not self.drag and not event.shift and not event.alt and not \
1393         event.ctrl:
1394             # select
1395             treshold = 10
1396             clicked = mathutils.Vector([event.mouse_region_x,
1397                 event.mouse_region_y])
1398             self.active_keyframe = False
1399             self.active_handle = False
1400             self.active_timebead = False
1401             self.active_frame = False
1402             context.window_manager.motion_trail.force_update = True
1403             context.window_manager.motion_trail.handle_type_enabled = True
1404             found = False
1405             
1406             if context.window_manager.motion_trail.path_before == 0:
1407                 frame_min = context.scene.frame_start
1408             else:
1409                 frame_min = max(context.scene.frame_start,
1410                     context.scene.frame_current - \
1411                     context.window_manager.motion_trail.path_before)
1412             if context.window_manager.motion_trail.path_after == 0:
1413                 frame_max = context.scene.frame_end
1414             else:
1415                 frame_max = min(context.scene.frame_end,
1416                     context.scene.frame_current + \
1417                     context.window_manager.motion_trail.path_after)
1418             
1419             for objectname, values in self.click.items():
1420                 if found:
1421                     break
1422                 for frame, type, coord, action_ob, child in values:
1423                     if frame < frame_min or frame > frame_max:
1424                         continue
1425                     if (coord - clicked).length <= treshold:
1426                         found = True
1427                         if type == "keyframe":
1428                             self.active_keyframe = [objectname, frame, frame,
1429                                 action_ob, child]
1430                         elif type == "handle_left":
1431                             self.active_handle = [objectname, frame, "left",
1432                                 action_ob, child]
1433                         elif type == "handle_right":
1434                             self.active_handle = [objectname, frame, "right",
1435                                 action_ob, child]
1436                         elif type == "timebead":
1437                             self.active_timebead = [objectname, frame, frame,
1438                                 action_ob, child]
1439                         elif type == "frame":
1440                             self.active_frame = [objectname, frame, frame,
1441                                 action_ob, child]
1442                         break
1443             if not found:
1444                 context.window_manager.motion_trail.handle_type_enabled = False
1445                 # no motion trail selections, so pass on to normal select()
1446                 if bpy.ops.view3d.select.poll():
1447                     bpy.ops.view3d.select('INVOKE_DEFAULT')
1448             else:
1449                 handle_type = get_handle_type(self.active_keyframe,
1450                     self.active_handle)
1451                 if handle_type:
1452                     context.window_manager.motion_trail.handle_type_old = \
1453                         handle_type
1454                     context.window_manager.motion_trail.handle_type = \
1455                         handle_type
1456                 else:
1457                     context.window_manager.motion_trail.handle_type_enabled = \
1458                         False
1459         elif event.type == 'LEFTMOUSE' and event.value == 'PRESS' and \
1460         self.drag:
1461             # stop drag
1462             self.drag = False
1463             self.lock = True
1464             context.window_manager.motion_trail.force_update = True
1465         elif event.type == 'LEFTMOUSE' and event.value == 'PRESS' and not\
1466         event.alt and not event.ctrl and not event.shift:
1467             if eval("bpy.ops."+self.left_action+".poll()"):
1468                 eval("bpy.ops."+self.left_action+"('INVOKE_DEFAULT')")
1469         
1470         if context.area: # not available if other window-type is fullscreen
1471             context.area.tag_redraw()
1472         
1473         return {'PASS_THROUGH'}
1474
1475     def invoke(self, context, event):
1476         if context.area.type == 'VIEW_3D':
1477             # get clashing keymap items
1478             select = context.user_preferences.inputs.select_mouse
1479             kms = [bpy.context.window_manager.keyconfigs.active.\
1480                 keymaps['3D View'], bpy.context.window_manager.keyconfigs.\
1481                 active.keymaps['Object Mode']]
1482             kmis = []
1483             self.left_action = None
1484             self.right_action = None
1485             for km in kms:
1486                 for kmi in km.keymap_items:
1487                     if kmi.idname == "transform.translate" and \
1488                     kmi.map_type == 'KEYBOARD' and not \
1489                     kmi.properties.texture_space:
1490                         kmis.append(kmi)
1491                         self.transform_key = kmi.type
1492                     elif (kmi.type == 'ACTIONMOUSE' and select == 'RIGHT') \
1493                     and not kmi.alt and not kmi.any and not kmi.ctrl \
1494                     and not kmi.shift:
1495                         kmis.append(kmi)
1496                         self.left_action = kmi.idname
1497                     elif kmi.type == 'SELECTMOUSE' and not kmi.alt and not \
1498                     kmi.any and not kmi.ctrl and not kmi.shift:
1499                         kmis.append(kmi)
1500                         if select == 'RIGHT':
1501                             self.right_action = kmi.idname
1502                         else:
1503                             self.left_action = kmi.idname
1504                     elif kmi.type == 'LEFTMOUSE' and not kmi.alt and not \
1505                     kmi.any and not kmi.ctrl and not kmi.shift:
1506                         kmis.append(kmi)
1507                         self.left_action = kmi.idname
1508             
1509             if context.window_manager.motion_trail.enabled == 0:
1510                 # enable
1511                 context.window_manager.motion_trail.enabled = 1
1512                 self.active_keyframe = False
1513                 self.active_handle = False
1514                 self.active_timebead = False
1515                 self.active_frame = False
1516                 self.click = {}
1517                 self.drag = False
1518                 self.lock = True
1519                 self.perspective = context.region_data.perspective_matrix
1520                 self.displayed = []
1521                 context.window_manager.motion_trail.force_update = True
1522                 context.window_manager.motion_trail.handle_type_enabled = False
1523                 self.cached = {"path":{}, "keyframes":{},
1524                     "timebeads_timing":{}, "timebeads_speed":{}}
1525                 
1526                 for kmi in kmis:
1527                     kmi.active = False
1528
1529                 self._handle1 = context.region.callback_add(calc_callback,
1530                     (self, context), 'POST_VIEW')
1531                 self._handle2 = context.region.callback_add(draw_callback,
1532                     (self, context), 'POST_PIXEL')
1533                 if context.area:
1534                     context.area.tag_redraw()
1535
1536                 context.window_manager.modal_handler_add(self)
1537             else:
1538                 # disable
1539                 context.window_manager.motion_trail.enabled = -1
1540                 for kmi in kmis:
1541                     kmi.active = True
1542             
1543             return {'RUNNING_MODAL'}
1544         
1545         else:
1546             self.report({'WARNING'}, "View3D not found, cannot run operator")
1547             return {'CANCELLED'}
1548
1549
1550 class MotionTrailPanel(bpy.types.Panel):
1551     bl_space_type = 'VIEW_3D'
1552     bl_region_type = 'TOOLS'
1553     bl_label = "Motion Trail"
1554     bl_options = {'DEFAULT_CLOSED'}
1555     @classmethod
1556     def poll(cls, context):
1557         if not context.active_object:
1558             return(False)
1559         return(context.active_object.mode in ['OBJECT', 'POSE'])
1560     
1561     def draw(self, context):
1562         col = self.layout.column()
1563         if context.window_manager.motion_trail.enabled != 1:
1564             col.operator("view3d.motion_trail", text="Enable motion trail")
1565         else:
1566             col.operator("view3d.motion_trail", text="Disable motion trail")
1567         
1568         box = self.layout.box()
1569         box.prop(context.window_manager.motion_trail, "mode")
1570         #box.prop(context.window_manager.motion_trail, "calculate")
1571         if context.window_manager.motion_trail.mode in ['timing']:
1572             box.prop(context.window_manager.motion_trail, "timebeads")
1573         
1574         box = self.layout.box()
1575         col = box.column()
1576         row = col.row()
1577         if context.window_manager.motion_trail.path_display:
1578             row.prop(context.window_manager.motion_trail, "path_display",
1579                 icon="DOWNARROW_HLT", text="", emboss=False)
1580         else:
1581             row.prop(context.window_manager.motion_trail, "path_display",
1582                 icon="RIGHTARROW", text="", emboss=False)
1583         row.label("Path options")
1584         if context.window_manager.motion_trail.path_display:
1585             col.prop(context.window_manager.motion_trail, "path_style",
1586                 text="Style")
1587             grouped = col.column(align=True)
1588             grouped.prop(context.window_manager.motion_trail, "path_width",
1589                 text="Width")
1590             grouped.prop(context.window_manager.motion_trail,
1591                 "path_transparency", text="Transparency")
1592             grouped.prop(context.window_manager.motion_trail,
1593                 "path_resolution")
1594             row = grouped.row(align=True)
1595             row.prop(context.window_manager.motion_trail, "path_before")
1596             row.prop(context.window_manager.motion_trail, "path_after")
1597             col = col.column(align=True)
1598             col.prop(context.window_manager.motion_trail, "keyframe_numbers")
1599             col.prop(context.window_manager.motion_trail, "frame_display")
1600         
1601         if context.window_manager.motion_trail.mode in ['location']:
1602             box = self.layout.box()
1603             col = box.column(align=True)
1604             col.prop(context.window_manager.motion_trail, "handle_display",
1605                 text="Handles")
1606             if context.window_manager.motion_trail.handle_display:
1607                 row = col.row()
1608                 row.enabled = context.window_manager.motion_trail.\
1609                     handle_type_enabled
1610                 row.prop(context.window_manager.motion_trail, "handle_type")
1611
1612
1613 class MotionTrailProps(bpy.types.PropertyGroup):
1614     def internal_update(self, context):
1615         context.window_manager.motion_trail.force_update = True
1616         if context.area:
1617             context.area.tag_redraw()
1618     
1619     # internal use
1620     enabled = bpy.props.IntProperty(default=0)
1621     force_update = bpy.props.BoolProperty(name="internal use",
1622         description="Force calc_callback to fully execute",
1623         default=False)
1624     handle_type_enabled = bpy.props.BoolProperty(default=False)
1625     handle_type_frame = bpy.props.FloatProperty()
1626     handle_type_side = bpy.props.StringProperty()
1627     handle_type_action_ob = bpy.props.StringProperty()
1628     handle_type_child = bpy.props.StringProperty()
1629     handle_type_old = bpy.props.EnumProperty(items=(("AUTO", "", ""), 
1630         ("AUTO_CLAMPED", "", ""), ("VECTOR", "", ""), ("ALIGNED", "", ""), 
1631         ("FREE", "", "")), default='AUTO',)
1632     
1633     # visible in user interface
1634     calculate = bpy.props.EnumProperty(name="Calculate",
1635         items=(("fast", "Fast", "Recommended setting, change if the "\
1636             "motion path is positioned incorrectly"),
1637             ("full", "Full", "Takes parenting and modifiers into account, "\
1638             "but can be very slow on complicated scenes")),
1639         description="Calculation method for determining locations",
1640         default='full',
1641         update=internal_update)
1642     frame_display = bpy.props.BoolProperty(name="Frames",
1643         description="Display frames, \n test",
1644         default=True,
1645         update=internal_update)
1646     handle_display = bpy.props.BoolProperty(name="Display",
1647         description="Display handles",
1648         default=True,
1649         update=internal_update)
1650     handle_type = bpy.props.EnumProperty(name="Type",
1651         items=(("AUTO", "Automatic", ""),
1652             ("AUTO_CLAMPED", "Auto Clamped", ""),
1653             ("VECTOR", "Vector", ""),
1654             ("ALIGNED", "Aligned", ""),
1655             ("FREE", "Free", "")),
1656         description="Set handle type for the selected handle",
1657         default='AUTO',
1658         update=set_handle_type)
1659     keyframe_numbers = bpy.props.BoolProperty(name="Keyframe numbers",
1660         description="Display keyframe numbers",
1661         default=False,
1662         update=internal_update)
1663     mode = bpy.props.EnumProperty(name="Mode",
1664         items=(("location", "Location", "Change path that is followed"),
1665             ("speed", "Speed", "Change speed between keyframes"),
1666             ("timing", "Timing", "Change position of keyframes on timeline")),
1667         description="Enable editing of certain properties in the 3d-view",
1668         default='location',
1669         update=internal_update)
1670     path_after = bpy.props.IntProperty(name="After",
1671         description="Number of frames to show after the current frame, "\
1672             "0 = display all",
1673         default=50,
1674         min=0,
1675         update=internal_update)
1676     path_before = bpy.props.IntProperty(name="Before",
1677         description="Number of frames to show before the current frame, "\
1678             "0 = display all",
1679         default=50,
1680         min=0,
1681         update=internal_update)
1682     path_display = bpy.props.BoolProperty(name="Path options",
1683         description="Display path options",
1684         default=True)
1685     path_resolution = bpy.props.IntProperty(name="Resolution",
1686         description="10 is smoothest, but could be "\
1687         "slow when adjusting keyframes, handles or timebeads",
1688         default=10,
1689         min=1,
1690         max=10,
1691         update=internal_update)
1692     path_style = bpy.props.EnumProperty(name="Path style",
1693         items=(("acceleration", "Acceleration", "Gradient based on relative "\
1694                 "acceleration"),
1695             ("simple", "Simple", "Black line"),
1696             ("speed", "Speed", "Gradient based on relative speed")),
1697         description="Information conveyed by path color",
1698         default='simple',
1699         update=internal_update)
1700     path_transparency = bpy.props.IntProperty(name="Path transparency",
1701         description="Determines visibility of path",
1702         default=0,
1703         min=0,
1704         max=100,
1705         subtype='PERCENTAGE',
1706         update=internal_update)
1707     path_width = bpy.props.IntProperty(name="Path width",
1708         description="Width in pixels",
1709         default=1,
1710         min=1,
1711         soft_max=5,
1712         update=internal_update)
1713     timebeads = bpy.props.IntProperty(name="Time beads",
1714         description="Number of time beads to display per segment",
1715         default=5,
1716         min=1,
1717         soft_max = 10,
1718         update=internal_update)
1719
1720
1721 classes = [MotionTrailProps,
1722     MotionTrailOperator,
1723     MotionTrailPanel]
1724
1725
1726 def register():
1727     for c in classes:
1728         bpy.utils.register_class(c)
1729     bpy.types.WindowManager.motion_trail = bpy.props.PointerProperty(\
1730         type=MotionTrailProps)
1731
1732
1733 def unregister():
1734     for c in classes:
1735         bpy.utils.unregister_class(c)
1736     del bpy.types.WindowManager.motion_trail
1737
1738
1739 if __name__ == "__main__":
1740     register()