From cc730b6ce338e2a52e07756b4c22be8676b32058 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Wed, 3 Dec 2025 17:44:05 -0800 Subject: [PATCH] Implement #136: Added support for exporting group properties for sequences --- io_scene_psk_psa/psa/export/operators.py | 41 ++++++++++++++++++----- io_scene_psk_psa/psa/export/properties.py | 17 ++++++++++ 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index 99a8fea..eccd916 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -18,7 +18,6 @@ from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions from psk_psa_py.psa.writer import write_psa_to_file from ...shared.helpers import populate_bone_collection_list, get_nla_strips_in_frame_range, PsxBoneCollection from ...shared.ui import draw_bone_filter_mode -from ...shared.types import PSX_PG_action_export, PSX_PG_scene_export def get_sequences_propnames_from_source(sequence_source: str) -> Tuple[str, str]: @@ -301,6 +300,7 @@ class PSA_OT_export(Operator, ExportHelper): def draw(self, context): layout = self.layout + assert layout pg = getattr(context.scene, 'psa_export') sequences_header, sequences_panel = layout.panel('Sequences', default_closed=False) @@ -334,7 +334,7 @@ class PSA_OT_export(Operator, ExportHelper): sequences_panel.template_list(PSA_UL_export_sequences.bl_idname, '', pg, propname, pg, active_propname, rows=max(3, min(len(getattr(pg, propname)), 10))) - name_header, name_panel = layout.panel('Name', default_closed=False) + name_header, name_panel = sequences_panel.panel('Name', default_closed=False) name_header.label(text='Name') if name_panel: flow = name_panel.grid_flow() @@ -351,8 +351,20 @@ class PSA_OT_export(Operator, ExportHelper): if count > 1: layout.label(text=f'Duplicate action: {action_name}', icon='ERROR') break + + # Group + group_header, group_panel = sequences_panel.panel('Group', default_closed=True) + group_header.label(text='Group') + if group_panel is not None: + group_flow = group_panel.grid_flow() + group_flow.use_property_split = True + group_flow.use_property_decorate = False + group_flow.prop(pg, 'group_source') + if pg.group_source == 'CUSTOM': + group_flow.prop(pg, 'group_custom', placeholder='Group') - sampling_header, sampling_panel = layout.panel('Data Source', default_closed=False) + # Sampling + sampling_header, sampling_panel = sequences_panel.panel('Data Source', default_closed=False) sampling_header.label(text='Sampling') if sampling_panel: flow = sampling_panel.grid_flow() @@ -431,6 +443,7 @@ class PSA_OT_export(Operator, ExportHelper): self._check_context(context) except RuntimeError as e: self.report({'ERROR_INVALID_CONTEXT'}, str(e)) + return {'CANCELLED'} pg: PSA_PG_export = getattr(context.scene, 'psa_export') @@ -468,6 +481,15 @@ class PSA_OT_export(Operator, ExportHelper): export_sequences: List[PsaBuildSequence] = [] + def get_export_sequence_group(group_source: str, group_custom: str | None, action: Action | None) -> str | None: + match group_source: + case 'ACTIONS': + return action.psa_export.group if action else None + case 'CUSTOM': + return group_custom + case _: + return None + match pg.sequence_source: case 'ACTIONS': for action_item in filter(lambda x: x.is_selected, pg.action_list): @@ -475,7 +497,7 @@ class PSA_OT_export(Operator, ExportHelper): continue export_sequence = PsaBuildSequence(context.active_object, animation_data) export_sequence.name = action_item.name - export_sequence.group = action_item.group + export_sequence.group = get_export_sequence_group(pg.group_source, pg.group_custom, action_item.action) export_sequence.nla_state.action = action_item.action export_sequence.nla_state.frame_start = action_item.frame_start export_sequence.nla_state.frame_end = action_item.frame_end @@ -485,12 +507,15 @@ class PSA_OT_export(Operator, ExportHelper): export_sequences.append(export_sequence) case 'TIMELINE_MARKERS': for marker_item in filter(lambda x: x.is_selected, pg.marker_list): + nla_strips_actions: List[Action] = [] + for nla_strip in get_nla_strips_in_frame_range(animation_data, marker_item.frame_start, marker_item.frame_end): + if nla_strip.action: + nla_strips_actions.append(nla_strip.action) export_sequence = PsaBuildSequence(context.active_object, animation_data) export_sequence.name = marker_item.name + export_sequence.group = get_export_sequence_group(pg.group_source, pg.group_custom, next(iter(nla_strips_actions), None)) 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_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_sequence.compression_ratio = get_sequence_compression_ratio(pg.compression_ratio_source, pg.compression_ratio_custom, nla_strips_actions) export_sequences.append(export_sequence) @@ -498,7 +523,7 @@ class PSA_OT_export(Operator, ExportHelper): for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list): export_sequence = PsaBuildSequence(context.active_object, animation_data) export_sequence.name = nla_strip_item.name - export_sequence.group = nla_strip_item.action.psa_export.group + export_sequence.group = get_export_sequence_group(pg.group_source, pg.group_custom, 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]) @@ -510,7 +535,7 @@ class PSA_OT_export(Operator, ExportHelper): export_sequence = PsaBuildSequence(active_action_item.armature_object, active_action_item.armature_object.animation_data) action = active_action_item.action export_sequence.name = action.name - export_sequence.group = action.psa_export.group + export_sequence.group = get_export_sequence_group(pg.group_source, pg.group_custom, action) export_sequence.nla_state.action = action export_sequence.nla_state.frame_start = int(action.frame_range[0]) export_sequence.nla_state.frame_end = int(action.frame_range[1]) diff --git a/io_scene_psk_psa/psa/export/properties.py b/io_scene_psk_psa/psa/export/properties.py index 058e07b..c3b533b 100644 --- a/io_scene_psk_psa/psa/export/properties.py +++ b/io_scene_psk_psa/psa/export/properties.py @@ -133,6 +133,11 @@ sampling_mode_items = ( ('SUBFRAME', 'Subframe', 'Sampling is performed by evaluating the bone poses at the subframe time.\n\nNot recommended unless you are also animating with subframes enabled.', 'SUBFRAME', 1), ) +group_source_items = ( + ('ACTIONS', 'Actions', '', 0), + ('CUSTOM', 'Custom', '', 1), +) + def sequence_source_update_cb(self: 'PSA_PG_export', context: Context) -> None: armature_objects = [] @@ -232,6 +237,18 @@ class PSA_PG_export(PropertyGroup, TransformMixin, ExportSpaceMixin, PsxBoneExpo items=sampling_mode_items, default='INTERPOLATED' ) + group_source: EnumProperty( + name='Group Source', + options=set(), + description='The source of the exported sequence\'s group property', + items=group_source_items, + default='ACTIONS' + ) + group_custom: StringProperty( + name='Group', + options=set(), + description='The group to apply to all exported sequences. Only applicable when Group Source is Custom.' + ) def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]: