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