Hello community,
I am trying to write a simple modal operator that blends objects from it’s current position to the position of the left or right keyframe. Something I use all the time when animating.
However there are some things I just don’t understand:
-
fcurve.keyframe_points.insert does not seem to update the scene. However updating the key directly with key.co.y does? Check lines 40/61. Without them, the view is not updated until the operator has finished AND i deselect something.
-
When I remove keys I need to deselect them before, otherwise they still exist until I deselect them. If I instead move them, they are not deleted but are greyed out, still work normally though. Check 56ff
-
When I confirm the operator and try to modify values afterwards using the nice ui in the bottom left of the 3d view, blender crashes. Crash report at the end of this post. Some properties seem to be deleted, but why?
bl_info = {
"name": "CL Anim Ops",
"author" : "Christoph Lendenfeld",
"blender": (2, 81, 0),
"category": "User",
}
import bpy
class BlendToNeighbour(bpy.types.Operator):
"""Blend current position to keyframes on each side""" # Use this as a tooltip for menu items and buttons.
bl_idname = "object.blend_to_neighbour" # Unique identifier for buttons and menu items to reference.
bl_label = "blend to neighbour" # Display name in the interface.
bl_options = {'REGISTER', 'UNDO'} # Enable undo for the operator.
#overshoot : bpy.props.BoolProperty(name = 'Overshoot', description = 'Allow values out of -1/1 range')
delta : bpy.props.FloatProperty(name = 'Blend Value', description = 'Amount of blending', min = -1, max = 1)
leftKey : bpy.props.IntProperty(name = 'Left Key', description= 'Left keyframe neighbour of current time', default = -64000)
rightKey : bpy.props.IntProperty(name = 'Right Key', description= 'Right keyframe neighbour of current time', default = 64000)
# calculate delta, interpolate and apply values here
def execute(self, context): # execute() is called when running the operator.
for fcurveInfo in self.fcurveInfos:
fcurve = fcurveInfo.fcurve
leftValue = fcurve.evaluate(self.leftKey)
rightValue = fcurve.evaluate(self.rightKey)
centerValue = fcurveInfo.startValue
blendValue = 0
if self.delta < 0:
blendValue = (abs(self.delta) * leftValue) + ((1 - abs(self.delta)) * centerValue)
else:
blendValue = (self.delta * rightValue) + ((1 - self.delta) * centerValue)
fcurveInfo.fcurve.keyframe_points.insert(self.currentFrame, blendValue)
fcurveInfo.key.co.y = blendValue # TODO this makes the scene update???
return {'FINISHED'} # Lets Blender know the operator finished successfully.
# called every frame?
# that's the interactive part
def modal(self, context, event):
if event.type == 'MOUSEMOVE': # Apply
self.delta = max(min((event.mouse_x - self.startPosX) / 100, 1),-1)
self.execute(context)
elif event.type == 'LEFTMOUSE': # Confirm
return {'FINISHED'}
elif event.type in {'RIGHTMOUSE', 'ESC'}: # Cancel
# delete key if it didn't exist before, or reset key to default position
for fcurveInfo in self.fcurveInfos:
fcurveInfo.key.select_control_point = False # TODO keys are not deleted as long as they are selected...
if fcurveInfo.newKey:
fcurveInfo.fcurve.keyframe_points.remove(fcurveInfo.key)
else:
fcurveInfo.fcurve.keyframe_points.insert(self.currentFrame, fcurveInfo.startValue)
fcurveInfo.key.co.y = fcurveInfo.startValue # TODO update scene
return {'CANCELLED'}
return {'RUNNING_MODAL'}
# called by blender before calling execute
# add the modal handler, which makes it interactive
def invoke(self, context, event):
self.startPosX = event.mouse_x
self.currentFrame = context.scene.frame_current
self.fcurveInfos = []
if context.mode == 'OBJECT':
self.selection = context.selected_objects
if context.mode == 'POSE':
self.selection = context.selected_pose_bones
if not self.selection:
self.info('Nothing selected')
return {'FINISHED'}
for obj in self.selection:
if not obj.animation_data:
continue
if not obj.animation_data.action:
continue
for fcurve in obj.animation_data.action.fcurves:
startValue = fcurve.evaluate(self.currentFrame)
newKey = self.findClosestFrames(fcurve)
key = fcurve.keyframe_points.insert(self.currentFrame, startValue)
self.fcurveInfos.append(FCurveInfo(fcurve, key, startValue, newKey))
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def info(self, info):
self.report({'INFO'}, str(info))
def findClosestFrames(self, fcurve):
newKey = True # False if current position already has a key
for key in fcurve.keyframe_points:
if key.co.x > self.leftKey and key.co.x < self.currentFrame:
self.leftKey = key.co.x
elif key.co.x < self.rightKey and key.co.x > self.currentFrame:
self.rightKey = key.co.x
elif key.co.x == self.currentFrame:
newKey = False
return newKey
# container for fcurves and keys
class FCurveInfo():
def __init__(self, fcurve, key, startValue, newKey):
self.fcurve = fcurve
self.key = key
self.startValue = startValue
self.newKey = newKey # bool that determines if the key was newly created
classes = (
BlendToNeighbour,
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
def unregister():
from bpy.utils import unregister_class
for cls in reversed(classes):
unregister_class(cls)
# Blender 2.81 (sub 16), Commit date: 2019-11-20 14:27, Hash 26bd5ebd42e3
bpy.ops.anim.keyframe_insert_menu(type='Location') # Operator
bpy.ops.transform.translate(value=(-2.62383, -2.69766e-07, 2.0078), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) # Operator
bpy.ops.transform.translate(value=(3.68477, -2.97356e-07, 2.21315), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) # Operator
bpy.ops.action.interpolation_type(type='BEZIER') # Operator
bpy.ops.object.blend_to_neighbour(delta=0.34, leftKey=20, rightKey=45) # Operator
bpy.data.window_managers["WinMan"].(null) = 0.37 # Property