Add a new operator, "Start Tweaking Strip Actions (Full Stack)", which
allows you to insert keyframes and preserve the pose that you visually
keyed while upper strips are evaluating,
The old operator has been renamed from "Start Tweaking Strip Actions" to
"Start Tweaking Strip Actions (Lower Stack)" and remains the default for
the hotkey {key TAB}.
**Limitations, Keyframe Remapping Failure Cases**:
1. For *transitions* above the tweaked strip, keyframe remapping will
fail for channel values that are affected by the transition. A work
around is to tweak the active strip without evaluating the upper NLA
stack.
It's not supported because it's non-trivial and I couldn't figure it
out for all transition combinations of blend modes. In the future, it
would be nice if transitions (and metas) supported nested tracks
instead of using the left/right strips for the transitions. That
would allow the transitioned strips to overlap in time. It would also
allow N strips to be part of the (previously) left and right strips,
or perhaps even N strips being transitioned in sequence (similar to a
blend tree). Proper keyframe remapping through all that is currently
beyond my mathematical ability. And even if I could figure it out,
would it make sense to keyframe remap through a transition?
//This case is reported to the user for failed keyframe insertions.//
2. Full replace upper strip that contains the keyed channels.
//This case is reported to the user for failed keyframe insertions.//
3. When the same action clip occurs multiple times (colored Red to
denote it's a linked strip) and vertically overlaps the tweaked
strip, then the remapping will generally fail and is expected to
fail.
I don't plan on adding support for this case as it's also non-trivial
and (hopefully) not a common or expected use case so it shouldn't be
much of an issue to lack support here.
For anyone curious on the cases that would work, it works when the
linked strips aren't time-aligned and when we can insert a keyframe
into the tweaked strip without modifying the current frame output of
the other linked strips. Having all frames sampled and the strip
non-time aligned leads to a working case. But if all key handles are
AUTO, then it's likely to fail.
//This case is not reported to the user for failed keyframe
insertions.//
4. When using Quaternions and a small strip influence on the tweaked
Combine strip. This was an existing failure case before this patch
too but worth a mention in case it causes confusion. D10504 has an
example file with instructions.
//This case is not reported to the user for failed keyframe insertions. //
5. When an upper Replace strip with high influence and animator keys to
Quaternion Combine (Replace is fine). This case is similar to (4)
where Quaternion 180 degree rotation limitations prevent a solution.
//This case is not reported to the user for failed keyframe insertions.//
Reviewed By: sybren, RiggingDojo
Differential Revision: https://developer.blender.org/D10504
349 lines
10 KiB
Python
349 lines
10 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
# <pep8 compliant>
|
|
|
|
from bpy.types import Header, Menu, Panel
|
|
from bpy.app.translations import contexts as i18n_contexts
|
|
from bl_ui.space_dopesheet import (
|
|
DopesheetFilterPopoverBase,
|
|
DopesheetActionPanelBase,
|
|
dopesheet_filter,
|
|
)
|
|
|
|
|
|
class NLA_HT_header(Header):
|
|
bl_space_type = 'NLA_EDITOR'
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
st = context.space_data
|
|
|
|
layout.template_header()
|
|
|
|
NLA_MT_editor_menus.draw_collapsible(context, layout)
|
|
|
|
layout.separator_spacer()
|
|
|
|
dopesheet_filter(layout, context)
|
|
|
|
layout.popover(
|
|
panel="NLA_PT_filters",
|
|
text="",
|
|
icon='FILTER',
|
|
)
|
|
|
|
layout.prop(st, "auto_snap", text="")
|
|
|
|
|
|
class NLA_PT_filters(DopesheetFilterPopoverBase, Panel):
|
|
bl_space_type = 'NLA_EDITOR'
|
|
bl_region_type = 'HEADER'
|
|
bl_label = "Filters"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
DopesheetFilterPopoverBase.draw_generic_filters(context, layout)
|
|
layout.separator()
|
|
DopesheetFilterPopoverBase.draw_search_filters(context, layout)
|
|
layout.separator()
|
|
DopesheetFilterPopoverBase.draw_standard_filters(context, layout)
|
|
|
|
|
|
class NLA_PT_action(DopesheetActionPanelBase, Panel):
|
|
bl_space_type = 'NLA_EDITOR'
|
|
bl_category = "Strip"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
strip = context.active_nla_strip
|
|
return strip and strip.type == 'CLIP' and strip.action
|
|
|
|
def draw(self, context):
|
|
action = context.active_nla_strip.action
|
|
self.draw_generic_panel(context, self.layout, action)
|
|
|
|
|
|
class NLA_MT_editor_menus(Menu):
|
|
bl_idname = "NLA_MT_editor_menus"
|
|
bl_label = ""
|
|
|
|
def draw(self, context):
|
|
st = context.space_data
|
|
layout = self.layout
|
|
layout.menu("NLA_MT_view")
|
|
layout.menu("NLA_MT_select")
|
|
if st.show_markers:
|
|
layout.menu("NLA_MT_marker")
|
|
layout.menu("NLA_MT_edit")
|
|
layout.menu("NLA_MT_add")
|
|
|
|
|
|
class NLA_MT_view(Menu):
|
|
bl_label = "View"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
st = context.space_data
|
|
|
|
layout.prop(st, "show_region_ui")
|
|
layout.separator()
|
|
|
|
layout.prop(st, "use_realtime_update")
|
|
|
|
layout.prop(st, "show_seconds")
|
|
layout.prop(st, "show_locked_time")
|
|
|
|
layout.prop(st, "show_strip_curves")
|
|
|
|
layout.separator()
|
|
layout.prop(st, "show_markers")
|
|
layout.prop(st, "show_local_markers")
|
|
|
|
layout.separator()
|
|
layout.operator("anim.previewrange_set")
|
|
layout.operator("anim.previewrange_clear")
|
|
layout.operator("nla.previewrange_set")
|
|
|
|
layout.separator()
|
|
layout.operator("nla.view_all")
|
|
layout.operator("nla.view_selected")
|
|
layout.operator("nla.view_frame")
|
|
|
|
layout.separator()
|
|
layout.menu("INFO_MT_area")
|
|
|
|
|
|
class NLA_MT_select(Menu):
|
|
bl_label = "Select"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
|
|
layout.operator("nla.select_all", text="All").action = 'SELECT'
|
|
layout.operator("nla.select_all", text="None").action = 'DESELECT'
|
|
layout.operator("nla.select_all", text="Invert").action = 'INVERT'
|
|
|
|
layout.separator()
|
|
layout.operator("nla.select_box").axis_range = False
|
|
layout.operator("nla.select_box", text="Box Select (Axis Range)").axis_range = True
|
|
|
|
layout.separator()
|
|
props = layout.operator("nla.select_leftright", text="Before Current Frame")
|
|
props.extend = False
|
|
props.mode = 'LEFT'
|
|
props = layout.operator("nla.select_leftright", text="After Current Frame")
|
|
props.extend = False
|
|
props.mode = 'RIGHT'
|
|
|
|
|
|
class NLA_MT_marker(Menu):
|
|
bl_label = "Marker"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
from bl_ui.space_time import marker_menu_generic
|
|
marker_menu_generic(layout, context)
|
|
|
|
|
|
class NLA_MT_marker_select(Menu):
|
|
bl_label = 'Select'
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
layout.operator("marker.select_all", text="All").action = 'SELECT'
|
|
layout.operator("marker.select_all", text="None").action = 'DESELECT'
|
|
layout.operator("marker.select_all", text="Invert").action = 'INVERT'
|
|
|
|
layout.separator()
|
|
|
|
layout.operator("marker.select_leftright", text="Before Current Frame").mode = 'LEFT'
|
|
layout.operator("marker.select_leftright", text="After Current Frame").mode = 'RIGHT'
|
|
|
|
|
|
|
|
class NLA_MT_edit(Menu):
|
|
bl_label = "Edit"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
scene = context.scene
|
|
|
|
layout.menu("NLA_MT_edit_transform", text="Transform")
|
|
|
|
layout.operator_menu_enum("nla.snap", "type", text="Snap")
|
|
|
|
layout.separator()
|
|
layout.operator("nla.duplicate", text="Duplicate").linked = False
|
|
layout.operator("nla.duplicate", text="Linked Duplicate").linked = True
|
|
layout.operator("nla.split")
|
|
layout.operator("nla.delete")
|
|
layout.operator("nla.tracks_delete")
|
|
|
|
layout.separator()
|
|
layout.operator("nla.mute_toggle")
|
|
|
|
layout.separator()
|
|
layout.operator("nla.apply_scale")
|
|
layout.operator("nla.clear_scale")
|
|
layout.operator("nla.action_sync_length").active = False
|
|
|
|
layout.separator()
|
|
layout.operator("nla.make_single_user")
|
|
|
|
layout.separator()
|
|
layout.operator("nla.swap")
|
|
layout.operator("nla.move_up")
|
|
layout.operator("nla.move_down")
|
|
|
|
# TODO: this really belongs more in a "channel" (or better, "track") menu
|
|
layout.separator()
|
|
layout.operator_menu_enum("anim.channels_move", "direction", text="Track Ordering...")
|
|
layout.operator("anim.channels_clean_empty")
|
|
|
|
layout.separator()
|
|
# TODO: names of these tools for 'tweak-mode' need changing?
|
|
if scene.is_nla_tweakmode:
|
|
layout.operator("nla.tweakmode_exit", text="Stop Editing Stashed Action").isolate_action = True
|
|
layout.operator("nla.tweakmode_exit", text="Stop Tweaking Strip Actions")
|
|
else:
|
|
layout.operator("nla.tweakmode_enter", text="Start Editing Stashed Action").isolate_action = True
|
|
layout.operator("nla.tweakmode_enter", text="Start Tweaking Strip Actions (Full Stack)").use_upper_stack_evaluation = True
|
|
layout.operator("nla.tweakmode_enter", text="Start Tweaking Strip Actions (Lower Stack)").use_upper_stack_evaluation = False
|
|
|
|
|
|
class NLA_MT_add(Menu):
|
|
bl_label = "Add"
|
|
bl_translation_context = i18n_contexts.operator_default
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
|
|
layout.operator("nla.actionclip_add")
|
|
layout.operator("nla.transition_add")
|
|
layout.operator("nla.soundclip_add")
|
|
|
|
layout.separator()
|
|
layout.operator("nla.meta_add")
|
|
layout.operator("nla.meta_remove")
|
|
|
|
layout.separator()
|
|
layout.operator("nla.tracks_add").above_selected = False
|
|
layout.operator("nla.tracks_add", text="Add Tracks Above Selected").above_selected = True
|
|
|
|
layout.separator()
|
|
layout.operator("nla.selected_objects_add")
|
|
|
|
|
|
class NLA_MT_edit_transform(Menu):
|
|
bl_label = "Transform"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
|
|
layout.operator("transform.translate", text="Move")
|
|
layout.operator("transform.transform", text="Extend").mode = 'TIME_EXTEND'
|
|
layout.operator("transform.transform", text="Scale").mode = 'TIME_SCALE'
|
|
|
|
|
|
class NLA_MT_snap_pie(Menu):
|
|
bl_label = "Snap"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
pie = layout.menu_pie()
|
|
|
|
pie.operator("nla.snap", text="Selection to Current Frame").type = 'CFRA'
|
|
pie.operator("nla.snap", text="Selection to Nearest Frame").type = 'NEAREST_FRAME'
|
|
pie.operator("nla.snap", text="Selection to Nearest Second").type = 'NEAREST_SECOND'
|
|
pie.operator("nla.snap", text="Selection to Nearest Marker").type = 'NEAREST_MARKER'
|
|
|
|
|
|
class NLA_MT_view_pie(Menu):
|
|
bl_label = "View"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
|
|
pie = layout.menu_pie()
|
|
pie.operator("nla.view_all")
|
|
pie.operator("nla.view_selected", icon='ZOOM_SELECTED')
|
|
pie.operator("nla.view_frame")
|
|
|
|
|
|
class NLA_MT_context_menu(Menu):
|
|
bl_label = "NLA Context Menu"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
scene = context.scene
|
|
|
|
if scene.is_nla_tweakmode:
|
|
layout.operator("nla.tweakmode_exit", text="Stop Editing Stashed Action").isolate_action = True
|
|
layout.operator("nla.tweakmode_exit", text="Stop Tweaking Strip Actions")
|
|
else:
|
|
layout.operator("nla.tweakmode_enter", text="Start Editing Stashed Action").isolate_action = True
|
|
layout.operator("nla.tweakmode_enter", text="Start Tweaking Strip Actions (Full Stack)").use_upper_stack_evaluation = True
|
|
layout.operator("nla.tweakmode_enter", text="Start Tweaking Strip Actions (Lower Stack)").use_upper_stack_evaluation = False
|
|
|
|
layout.separator()
|
|
|
|
props = layout.operator("wm.call_panel", text="Rename...")
|
|
props.name = "TOPBAR_PT_name"
|
|
props.keep_open = False
|
|
layout.operator("nla.duplicate", text="Duplicate").linked = False
|
|
layout.operator("nla.duplicate", text="Linked Duplicate").linked = True
|
|
|
|
layout.separator()
|
|
|
|
layout.operator("nla.split")
|
|
layout.operator("nla.delete")
|
|
|
|
layout.separator()
|
|
|
|
layout.operator("nla.swap")
|
|
|
|
layout.separator()
|
|
|
|
layout.operator_menu_enum("nla.snap", "type", text="Snap")
|
|
|
|
|
|
class NLA_MT_channel_context_menu(Menu):
|
|
bl_label = "NLA Channel Context Menu"
|
|
|
|
def draw(self, _context):
|
|
layout = self.layout
|
|
|
|
layout.operator_menu_enum("anim.channels_move", "direction", text="Track Ordering...")
|
|
layout.operator("anim.channels_clean_empty")
|
|
|
|
|
|
classes = (
|
|
NLA_HT_header,
|
|
NLA_MT_edit,
|
|
NLA_MT_editor_menus,
|
|
NLA_MT_view,
|
|
NLA_MT_select,
|
|
NLA_MT_marker,
|
|
NLA_MT_marker_select,
|
|
NLA_MT_add,
|
|
NLA_MT_edit_transform,
|
|
NLA_MT_snap_pie,
|
|
NLA_MT_view_pie,
|
|
NLA_MT_context_menu,
|
|
NLA_MT_channel_context_menu,
|
|
NLA_PT_filters,
|
|
NLA_PT_action,
|
|
)
|
|
|
|
if __name__ == "__main__": # only for live edit.
|
|
from bpy.utils import register_class
|
|
for cls in classes:
|
|
register_class(cls)
|