From b20d19d072236da3975a21ef456c0b8b7c4e8e40 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Fri, 11 Aug 2023 02:09:16 -0700 Subject: [PATCH] Added NLA track sequence source option (reimplementation of `feature-nla-track-sequence-source` branch). This just needs a bit of time in the oven to test it out. --- io_scene_psk_psa/helpers.py | 2 +- io_scene_psk_psa/psa/export/operators.py | 85 +++++++++++++---------- io_scene_psk_psa/psa/export/properties.py | 73 +++++++++++++++++-- io_scene_psk_psa/psa/export/ui.py | 2 +- 4 files changed, 118 insertions(+), 44 deletions(-) diff --git a/io_scene_psk_psa/helpers.py b/io_scene_psk_psa/helpers.py index fe683e0..d7b7980 100644 --- a/io_scene_psk_psa/helpers.py +++ b/io_scene_psk_psa/helpers.py @@ -34,7 +34,7 @@ def rgb_to_srgb(c: float): return 12.92 * c -def get_nla_strips_in_timeframe(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]: +def get_nla_strips_in_frame_range(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]: if animation_data is None: return [] strips = [] diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index 407d144..a6f1a04 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -8,10 +8,10 @@ from bpy.types import Context, Armature, Action, Object, AnimData, TimelineMarke from bpy_extras.io_utils import ExportHelper from bpy_types import Operator -from io_scene_psk_psa.helpers import populate_bone_group_list, get_nla_strips_in_timeframe -from io_scene_psk_psa.psa.builder import build_psa, PsaBuildSequence, PsaBuildOptions -from io_scene_psk_psa.psa.export.properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences -from io_scene_psk_psa.psa.writer import write_psa +from .properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences +from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions +from ..writer import write_psa +from ...helpers import populate_bone_group_list, get_nla_strips_in_frame_range def is_action_for_armature(armature: Armature, action: Action): @@ -150,7 +150,7 @@ def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context: if next_marker_index < len(sorted_timeline_markers): # There is a next marker. Use that next marker's frame position as the last frame of this sequence. frame_end = sorted_timeline_markers[next_marker_index].frame - nla_strips = get_nla_strips_in_timeframe(animation_data, marker.frame, frame_end) + nla_strips = get_nla_strips_in_frame_range(animation_data, marker.frame, frame_end) if len(nla_strips) > 0: frame_end = min(frame_end, max(map(lambda nla_strip: nla_strip.frame_end, nla_strips))) frame_start = max(frame_start, min(map(lambda nla_strip: nla_strip.frame_start, nla_strips))) @@ -254,12 +254,18 @@ class PSA_OT_export(Operator, ExportHelper): # SOURCE layout.prop(pg, 'sequence_source', text='Source') - if pg.sequence_source == 'TIMELINE_MARKERS': + if pg.sequence_source in {'TIMELINE_MARKERS', 'NLA_TRACK_STRIPS'}: # ANIMDATA SOURCE layout.prop(pg, 'should_override_animation_data') if pg.should_override_animation_data: layout.prop(pg, 'animation_data_override', text='') + if pg.sequence_source == 'NLA_TRACK_STRIPS': + flow = layout.grid_flow() + flow.use_property_split = True + flow.use_property_decorate = False + flow.prop(pg, 'nla_track') + # SELECT ALL/NONE row = layout.row(align=True) row.label(text='Select') @@ -269,25 +275,19 @@ class PSA_OT_export(Operator, ExportHelper): # ACTIONS if pg.sequence_source == 'ACTIONS': rows = max(3, min(len(pg.action_list), 10)) - layout.template_list('PSA_UL_export_sequences', '', pg, 'action_list', pg, 'action_list_index', rows=rows) - - col = layout.column() - col.use_property_split = True - col.use_property_decorate = False - col.prop(pg, 'sequence_name_prefix') - col.prop(pg, 'sequence_name_suffix') - elif pg.sequence_source == 'TIMELINE_MARKERS': rows = max(3, min(len(pg.marker_list), 10)) - layout.template_list('PSA_UL_export_sequences', '', pg, 'marker_list', pg, 'marker_list_index', - rows=rows) + layout.template_list('PSA_UL_export_sequences', '', pg, 'marker_list', pg, 'marker_list_index', rows=rows) + elif pg.sequence_source == 'NLA_TRACK_STRIPS': + rows = max(3, min(len(pg.nla_strip_list), 10)) + layout.template_list('PSA_UL_export_sequences', '', pg, 'nla_strip_list', pg, 'nla_strip_list_index', rows=rows) - col = layout.column() - col.use_property_split = True - col.use_property_decorate = False - col.prop(pg, 'sequence_name_prefix') - col.prop(pg, 'sequence_name_suffix') + col = layout.column() + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'sequence_name_prefix') + col.prop(pg, 'sequence_name_suffix') # Determine if there is going to be a naming conflict and display an error, if so. selected_items = [x for x in pg.action_list if x.is_selected] @@ -360,6 +360,8 @@ class PSA_OT_export(Operator, ExportHelper): raise RuntimeError('No actions were selected for export') elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_names) == 0: raise RuntimeError('No timeline markers were selected for export') + elif pg.sequence_source == 'NLA_TRACK_STRIPS' and len(pg.nla_strip_list) == 0: + raise RuntimeError('No NLA track strips were selected for export') # Populate the export sequence list. animation_data_object = get_animation_data_object(context) @@ -371,29 +373,38 @@ class PSA_OT_export(Operator, ExportHelper): export_sequences: List[PsaBuildSequence] = [] if pg.sequence_source == 'ACTIONS': - for action in filter(lambda x: x.is_selected, pg.action_list): - if len(action.action.fcurves) == 0: + for action_item in filter(lambda x: x.is_selected, pg.action_list): + if len(action_item.action.fcurves) == 0: continue export_sequence = PsaBuildSequence() - export_sequence.nla_state.action = action.action - export_sequence.name = action.name - export_sequence.nla_state.frame_start = action.frame_start - export_sequence.nla_state.frame_end = action.frame_end - export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action.action]) - export_sequence.compression_ratio = action.action.psa_export.compression_ratio - export_sequence.key_quota = action.action.psa_export.key_quota + export_sequence.nla_state.action = action_item.action + export_sequence.name = action_item.name + export_sequence.nla_state.frame_start = action_item.frame_start + export_sequence.nla_state.frame_end = action_item.frame_end + export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action_item.action]) + export_sequence.compression_ratio = action_item.action.psa_export.compression_ratio + export_sequence.key_quota = action_item.action.psa_export.key_quota export_sequences.append(export_sequence) elif pg.sequence_source == 'TIMELINE_MARKERS': - for marker in pg.marker_list: + for marker_item in filter(lambda x: x.is_selected, pg.marker_list): export_sequence = PsaBuildSequence() - export_sequence.name = marker.name + export_sequence.name = marker_item.name export_sequence.nla_state.action = None - export_sequence.nla_state.frame_start = marker.frame_start - export_sequence.nla_state.frame_end = marker.frame_end + export_sequence.nla_state.frame_start = marker_item.frame_start + export_sequence.nla_state.frame_end = marker_item.frame_end nla_strips_actions = set( - map(lambda x: x.action, get_nla_strips_in_timeframe(animation_data, marker.frame_start, marker.frame_end))) + map(lambda x: x.action, get_nla_strips_in_frame_range(animation_data, marker_item.frame_start, marker_item.frame_end))) export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, nla_strips_actions) export_sequences.append(export_sequence) + elif pg.sequence_source == 'NLA_TRACK_STRIPS': + for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list): + export_sequence = PsaBuildSequence() + export_sequence.name = nla_strip_item.name + export_sequence.nla_state.action = nla_strip_item.action + export_sequence.nla_state.frame_start = nla_strip_item.frame_start + export_sequence.nla_state.frame_end = nla_strip_item.frame_end + export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [nla_strip_item.action]) + export_sequences.append(export_sequence) else: raise ValueError(f'Unhandled sequence source: {pg.sequence_source}') @@ -432,6 +443,8 @@ class PSA_OT_export_actions_select_all(Operator): return pg.action_list elif pg.sequence_source == 'TIMELINE_MARKERS': return pg.marker_list + elif pg.sequence_source == 'NLA_TRACK_STRIPS': + return pg.nla_strip_list return None @classmethod @@ -463,6 +476,8 @@ class PSA_OT_export_actions_deselect_all(Operator): return pg.action_list elif pg.sequence_source == 'TIMELINE_MARKERS': return pg.marker_list + elif pg.sequence_source == 'NLA_TRACK_STRIPS': + return pg.nla_strip_list return None @classmethod diff --git a/io_scene_psk_psa/psa/export/properties.py b/io_scene_psk_psa/psa/export/properties.py index 1f75f74..5a87548 100644 --- a/io_scene_psk_psa/psa/export/properties.py +++ b/io_scene_psk_psa/psa/export/properties.py @@ -1,10 +1,11 @@ +import re import sys from fnmatch import fnmatch -from typing import List +from typing import List, Optional from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty, \ StringProperty -from bpy.types import PropertyGroup, Object, Action +from bpy.types import PropertyGroup, Object, Action, AnimData, Context from ...types import PSX_PG_bone_group_list_item @@ -25,7 +26,7 @@ class PSA_PG_export_action_list_item(PropertyGroup): is_pose_marker: BoolProperty(options={'HIDDEN'}) -class PSA_PG_export_timeline_markers(PropertyGroup): +class PSA_PG_export_timeline_markers(PropertyGroup): # TODO: rename this to singular marker_index: IntProperty() name: StringProperty() is_selected: BoolProperty(default=True) @@ -33,6 +34,51 @@ class PSA_PG_export_timeline_markers(PropertyGroup): frame_end: IntProperty(options={'HIDDEN'}) +class PSA_PG_export_nla_strip_list_item(PropertyGroup): + name: StringProperty() + action: PointerProperty(type=Action) + frame_start: FloatProperty() + frame_end: FloatProperty() + is_selected: BoolProperty(default=True) + + +def nla_track_update_cb(self: 'PSA_PG_export', context: Context) -> None: + self.nla_strip_list.clear() + if context.object is None or context.object.animation_data is None: + return + match = re.match(r'^(\d+).+$', self.nla_track) + self.nla_track_index = int(match.group(1)) if match else -1 + if self.nla_track_index >= 0: + nla_track = context.object.animation_data.nla_tracks[self.nla_track_index] + for nla_strip in nla_track.strips: + strip: PSA_PG_export_nla_strip_list_item = self.nla_strip_list.add() + strip.action = nla_strip.action + strip.name = nla_strip.name + strip.frame_start = nla_strip.frame_start + strip.frame_end = nla_strip.frame_end + + +def get_animation_data(pg: 'PSA_PG_export', context: Context) -> Optional[AnimData]: + animation_data_object = context.object + if pg.should_override_animation_data: + animation_data_object = pg.animation_data_override + return animation_data_object.animation_data if animation_data_object else None + + +def nla_track_search_cb(self, context: Context, edit_text: str): + pg = getattr(context.scene, 'psa_export') + animation_data = get_animation_data(pg, context) + if animation_data is None: + return + for index, nla_track in enumerate(animation_data.nla_tracks): + yield f'{index} - {nla_track.name}' + + +def animation_data_override_update_cb(self: 'PSA_PG_export', context: Context): + # Reset NLA track selection + self.nla_track = '' + + class PSA_PG_export(PropertyGroup): root_motion: BoolProperty( name='Root Motion', @@ -46,10 +92,12 @@ class PSA_PG_export(PropertyGroup): name='Override Animation Data', options=empty_set, default=False, - description='Use the animation data from a different object instead of the selected object' + description='Use the animation data from a different object instead of the selected object', + update=animation_data_override_update_cb, ) animation_data_override: PointerProperty( type=Object, + update=animation_data_override_update_cb, poll=psa_export_property_group_animation_data_override_poll ) sequence_source: EnumProperty( @@ -58,10 +106,18 @@ class PSA_PG_export(PropertyGroup): description='', items=( ('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0), - ('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences will be exported using timeline markers', 'MARKER_HLT', - 1), + ('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences are delineated by scene timeline markers', 'MARKER_HLT', 1), + ('NLA_TRACK_STRIPS', 'NLA Track Strips', 'Sequences are delineated by the start & end times of strips on the selected NLA track', 'NLA', 2) ) ) + nla_track: StringProperty( + name='NLA Track', + options=empty_set, + description='', + search=nla_track_search_cb, + update=nla_track_update_cb + ) + nla_track_index: IntProperty(name='NLA Track Index', default=-1) fps_source: EnumProperty( name='FPS Source', options=empty_set, @@ -80,6 +136,8 @@ class PSA_PG_export(PropertyGroup): action_list_index: IntProperty(default=0) marker_list: CollectionProperty(type=PSA_PG_export_timeline_markers) marker_list_index: IntProperty(default=0) + nla_strip_list: CollectionProperty(type=PSA_PG_export_nla_strip_list_item) + nla_strip_list_index: IntProperty(default=0) bone_filter_mode: EnumProperty( name='Bone Filter', options=empty_set, @@ -145,7 +203,7 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]: if not pg.sequence_filter_asset: for i, sequence in enumerate(sequences): - if hasattr(sequence, 'action') and sequence.action.asset_data is not None: + if hasattr(sequence, 'action') and sequence.action is not None and sequence.action.asset_data is not None: flt_flags[i] &= ~bitflag_filter_item if not pg.sequence_filter_pose_marker: @@ -164,5 +222,6 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]: classes = ( PSA_PG_export_action_list_item, PSA_PG_export_timeline_markers, + PSA_PG_export_nla_strip_list_item, PSA_PG_export, ) diff --git a/io_scene_psk_psa/psa/export/ui.py b/io_scene_psk_psa/psa/export/ui.py index ad9d29a..d302265 100644 --- a/io_scene_psk_psa/psa/export/ui.py +++ b/io_scene_psk_psa/psa/export/ui.py @@ -16,7 +16,7 @@ class PSA_UL_export_sequences(UIList): item = cast(PSA_PG_export_action_list_item, item) is_pose_marker = hasattr(item, 'is_pose_marker') and item.is_pose_marker layout.prop(item, 'is_selected', icon_only=True, text=item.name) - if hasattr(item, 'action') and item.action.asset_data is not None: + if hasattr(item, 'action') and item.action is not None and item.action.asset_data is not None: layout.label(text='', icon='ASSET_MANAGER') row = layout.row(align=True)