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