From c6729416637ff9301541537b9e66d651a8204f78 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Mon, 24 Jan 2022 21:50:34 -0800 Subject: [PATCH] PSA Import screen now has more robust functionality now (but still aint done!) --- io_scene_psk_psa/__init__.py | 12 +- io_scene_psk_psa/helpers.py | 7 +- io_scene_psk_psa/psa/builder.py | 5 +- io_scene_psk_psa/psa/exporter.py | 59 ++++--- io_scene_psk_psa/psa/importer.py | 255 +++++++++++++++++++++++++------ io_scene_psk_psa/psk/exporter.py | 4 +- io_scene_psk_psa/psk/importer.py | 6 +- io_scene_psk_psa/types.py | 6 +- 8 files changed, 267 insertions(+), 87 deletions(-) diff --git a/io_scene_psk_psa/__init__.py b/io_scene_psk_psa/__init__.py index 1a98c9b..36110a0 100644 --- a/io_scene_psk_psa/__init__.py +++ b/io_scene_psk_psa/__init__.py @@ -13,6 +13,7 @@ bl_info = { if 'bpy' in locals(): import importlib + importlib.reload(psx_data) importlib.reload(psx_helpers) importlib.reload(psx_types) @@ -42,15 +43,14 @@ else: from .psa import reader as psa_reader from .psa import importer as psa_importer - import bpy from bpy.props import PointerProperty -classes = psx_types.__classes__ + \ - psk_importer.__classes__ + \ - psk_exporter.__classes__ + \ - psa_exporter.__classes__ + \ - psa_importer.__classes__ +classes = (psx_types.classes + + psk_importer.classes + + psk_exporter.classes + + psa_exporter.classes + + psa_importer.classes) def psk_export_menu_func(self, context): diff --git a/io_scene_psk_psa/helpers.py b/io_scene_psk_psa/helpers.py index 34b74cb..1217dca 100644 --- a/io_scene_psk_psa/helpers.py +++ b/io_scene_psk_psa/helpers.py @@ -17,8 +17,11 @@ def populate_bone_group_list(armature_object, bone_group_list): item.is_selected = True -def add_bone_groups_to_layout(layout): - pass +def get_psa_sequence_name(action, should_use_original_sequence_name): + if should_use_original_sequence_name and 'original_sequence_name' in action: + return action['original_sequence_name'] + else: + return action.name def get_export_bone_indices_for_bone_groups(armature_object, bone_group_indices: List[int]) -> List[int]: diff --git a/io_scene_psk_psa/psa/builder.py b/io_scene_psk_psa/psa/builder.py index cddc250..87549fb 100644 --- a/io_scene_psk_psa/psa/builder.py +++ b/io_scene_psk_psa/psa/builder.py @@ -108,10 +108,7 @@ class PsaBuilder(object): sequence = Psa.Sequence() - if options.should_use_original_sequence_names and 'original_sequence_name' in action: - sequence_name = action['original_sequence_name'] - else: - sequence_name = action.name + sequence_name = get_psa_sequence_name(action, options.should_use_original_sequence_names) sequence.name = bytes(sequence_name, encoding='windows-1252') sequence.frame_count = frame_max - frame_min + 1 diff --git a/io_scene_psk_psa/psa/exporter.py b/io_scene_psk_psa/psa/exporter.py index d53cfb2..1dd7c6e 100644 --- a/io_scene_psk_psa/psa/exporter.py +++ b/io_scene_psk_psa/psa/exporter.py @@ -1,5 +1,5 @@ import bpy -from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup +from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup, Panel from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty, EnumProperty from bpy_extras.io_utils import ExportHelper from typing import Type @@ -7,6 +7,7 @@ from .builder import PsaBuilder, PsaBuilderOptions from .data import * from ..types import BoneGroupListItem from ..helpers import * +from collections import Counter import re @@ -49,15 +50,13 @@ def update_action_names(context): property_group = context.scene.psa_export for item in property_group.action_list: action = item.action - if property_group.should_use_original_sequence_names and 'original_sequence_name' in action: - item.action_name = action['original_sequence_name'] - else: - item.action_name = action.name + item.action_name = get_psa_sequence_name(action, property_group.should_use_original_sequence_names) def should_use_original_sequence_names_updated(property, context): update_action_names(context) + class PsaExportPropertyGroup(PropertyGroup): action_list: CollectionProperty(type=PsaExportActionListItem) action_list_index: IntProperty(default=0) @@ -71,11 +70,11 @@ class PsaExportPropertyGroup(PropertyGroup): ) bone_group_list: CollectionProperty(type=BoneGroupListItem) bone_group_list_index: IntProperty(default=0) - should_use_original_sequence_names: BoolProperty(default=False, description='If the action was imported from the PSA Import panel, the original name of the action will be used instead of the action name assigned in Blender', update=should_use_original_sequence_names_updated) + should_use_original_sequence_names: BoolProperty(default=False, name='Original Names', description='If the action was imported from the PSA Import panel, the original name of the sequence will be used instead of the Blender action name', update=should_use_original_sequence_names_updated) def is_bone_filter_mode_item_available(context, identifier): - if identifier == "BONE_GROUPS": + if identifier == 'BONE_GROUPS': obj = context.active_object if not obj.pose or not obj.pose.bone_groups: return False @@ -83,7 +82,7 @@ def is_bone_filter_mode_item_available(context, identifier): class PsaExportOperator(Operator, ExportHelper): - bl_idname = 'export.psa' + bl_idname = 'psa_export.operator' bl_label = 'Export' __doc__ = 'Export actions to PSA' filename_ext = '.psa' @@ -102,19 +101,34 @@ class PsaExportOperator(Operator, ExportHelper): property_group = context.scene.psa_export # ACTIONS - box = layout.box() - box.label(text='Actions', icon='ACTION') - row = box.row() - row.template_list('PSA_UL_ExportActionList', 'asd', property_group, 'action_list', property_group, 'action_list_index', rows=10) - row = box.row(align=True) + layout.label(text='Actions', icon='ACTION') + row = layout.row(align=True) row.label(text='Select') row.operator('psa_export.actions_select_all', text='All') row.operator('psa_export.actions_deselect_all', text='None') + row = layout.row() + rows = max(3, min(len(property_group.action_list), 10)) + row.template_list('PSA_UL_ExportActionList', '', property_group, 'action_list', property_group, + 'action_list_index', rows=rows) - layout.prop(property_group, 'should_use_original_sequence_names', text='Original Sequence Names') + col = layout.column(heading="Options") + col.use_property_split = True + col.use_property_decorate = False + col.prop(property_group, 'should_use_original_sequence_names') + + # Determine if there is going to be a naming conflict and display an error, if so. + selected_actions = [x for x in property_group.action_list if x.is_selected] + action_names = [x.action_name for x in selected_actions] + action_name_counts = Counter(action_names) + for action_name, count in action_name_counts.items(): + if count > 1: + layout.label(text=f'Duplicate action: {action_name}', icon='ERROR') + break + + layout.separator() # BONES - box = layout.box() + box = layout.row() box.label(text='Bones', icon='BONE_DATA') bone_filter_mode_items = property_group.bl_rna.properties['bone_filter_mode'].enum_items_static row = box.row(align=True) @@ -126,10 +140,9 @@ class PsaExportOperator(Operator, ExportHelper): item_layout.enabled = is_bone_filter_mode_item_available(context, identifier) if property_group.bone_filter_mode == 'BONE_GROUPS': - box = layout.box() - row = box.row() rows = max(3, min(len(property_group.bone_group_list), 10)) - row.template_list('PSX_UL_BoneGroupList', '', property_group, 'bone_group_list', property_group, 'bone_group_list_index', rows=rows) + layout.template_list('PSX_UL_BoneGroupList', '', property_group, 'bone_group_list', property_group, + 'bone_group_list_index', rows=rows) def is_action_for_armature(self, action): if len(action.fcurves) == 0: @@ -160,12 +173,12 @@ class PsaExportOperator(Operator, ExportHelper): # Populate actions list. property_group.action_list.clear() for action in bpy.data.actions: + if not self.is_action_for_armature(action): + continue item = property_group.action_list.add() item.action = action item.action_name = action.name - - if self.is_action_for_armature(action): - item.is_selected = True + item.is_selected = True update_action_names(context) @@ -264,11 +277,11 @@ class PsaExportDeselectAll(bpy.types.Operator): return {'FINISHED'} -__classes__ = [ +classes = ( PsaExportActionListItem, PsaExportPropertyGroup, PsaExportOperator, PSA_UL_ExportActionList, PsaExportSelectAll, PsaExportDeselectAll, -] +) diff --git a/io_scene_psk_psa/psa/importer.py b/io_scene_psk_psa/psa/importer.py index fa177c0..793721b 100644 --- a/io_scene_psk_psa/psa/importer.py +++ b/io_scene_psk_psa/psa/importer.py @@ -5,7 +5,7 @@ from mathutils import Vector, Quaternion, Matrix from .data import Psa from typing import List, AnyStr, Optional from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature, FileSelectParams -from bpy_extras.io_utils import ExportHelper, ImportHelper +from bpy_extras.io_utils import ImportHelper from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty from .reader import PsaReader @@ -16,6 +16,7 @@ class PsaImportOptions(object): self.should_use_fake_user = False self.should_stash = False self.sequence_names = [] + self.should_use_action_name_prefix = False self.action_name_prefix = '' @@ -216,40 +217,57 @@ class PsaImportActionListItem(PropertyGroup): return self.action_name -def on_psa_file_path_updated(property, context): +def load_psa_file(context): property_group = context.scene.psa_import - property_group.action_list.clear() + property_group.sequence_list.clear() property_group.psa_bones.clear() + property_group.action_list.clear() + property_group.psa_error = '' try: # Read the file and populate the action list. p = os.path.abspath(property_group.psa_file_path) psa_reader = PsaReader(p) for sequence in psa_reader.sequences.values(): - item = property_group.action_list.add() + item = property_group.sequence_list.add() item.action_name = sequence.name.decode('windows-1252') item.frame_count = sequence.frame_count - item.is_selected = True for psa_bone in psa_reader.bones: item = property_group.psa_bones.add() - item.bone_name = psa_bone.name - except IOError as e: - # TODO: set an error somewhere so the user knows the PSA could not be read. - pass + item.bone_name = psa_bone.name.decode('windows-1252') + except Exception as e: + property_group.psa_error = str(e) -class PsaImportPropertyGroup(bpy.types.PropertyGroup): +def on_psa_file_path_updated(property, context): + load_psa_file(context) + + +class PsaBonePropertyGroup(PropertyGroup): + bone_name: StringProperty() + + +class PsaDataPropertyGroup(PropertyGroup): + bone_count: IntProperty(default=0) + bones: CollectionProperty(type=PsaBonePropertyGroup) + sequence_count: IntProperty(default=0) + + +class PsaImportPropertyGroup(PropertyGroup): psa_file_path: StringProperty(default='', update=on_psa_file_path_updated, name='PSA File Path') + psa_error: StringProperty(default='') psa_bones: CollectionProperty(type=PsaImportPsaBoneItem) + sequence_list: CollectionProperty(type=PsaImportActionListItem) + sequence_list_index: IntProperty(name='', default=0) action_list: CollectionProperty(type=PsaImportActionListItem) action_list_index: IntProperty(name='', default=0) - action_filter_name: StringProperty(default='') should_clean_keys: BoolProperty(default=True, name='Clean Keyframes', description='Exclude unnecessary keyframes from being written to the actions.') should_use_fake_user: BoolProperty(default=True, name='Fake User', description='Assign each imported action a fake user so that the data block is saved even it has no users.') should_stash: BoolProperty(default=False, name='Stash', description='Stash each imported action as a strip on a new non-contributing NLA track') - action_name_prefix: StringProperty(default='', name='Action Name Prefix') + should_use_action_name_prefix: BoolProperty(default=False, name='Prefix Action Name') + action_name_prefix: StringProperty(default='', name='Prefix') -class PSA_UL_ImportActionList(UIList): +class PSA_UL_SequenceList(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): row = layout.row(align=True) @@ -282,7 +300,34 @@ class PSA_UL_ImportActionList(UIList): return flt_flags, flt_neworder -class PsaImportSelectAll(bpy.types.Operator): +class PSA_UL_ImportSequenceList(PSA_UL_SequenceList, UIList): + pass + + +class PSA_UL_ImportActionList(PSA_UL_SequenceList, UIList): + pass + + +class PsaImportSequencesSelectAll(bpy.types.Operator): + bl_idname = 'psa_import.sequences_select_all' + bl_label = 'All' + bl_description = 'Select all sequences' + + @classmethod + def poll(cls, context): + property_group = context.scene.psa_import + sequence_list = property_group.sequence_list + has_unselected_actions = any(map(lambda action: not action.is_selected, sequence_list)) + return len(sequence_list) > 0 and has_unselected_actions + + def execute(self, context): + property_group = context.scene.psa_import + for action in property_group.sequence_list: + action.is_selected = True + return {'FINISHED'} + + +class PsaImportActionsSelectAll(bpy.types.Operator): bl_idname = 'psa_import.actions_select_all' bl_label = 'All' bl_description = 'Select all actions' @@ -301,7 +346,26 @@ class PsaImportSelectAll(bpy.types.Operator): return {'FINISHED'} -class PsaImportDeselectAll(bpy.types.Operator): +class PsaImportSequencesDeselectAll(bpy.types.Operator): + bl_idname = 'psa_import.sequences_deselect_all' + bl_label = 'None' + bl_description = 'Deselect all sequences' + + @classmethod + def poll(cls, context): + property_group = context.scene.psa_import + sequence_list = property_group.sequence_list + has_selected_sequences = any(map(lambda action: action.is_selected, sequence_list)) + return len(sequence_list) > 0 and has_selected_sequences + + def execute(self, context): + property_group = context.scene.psa_import + for action in property_group.sequence_list: + action.is_selected = False + return {'FINISHED'} + + +class PsaImportActionsDeselectAll(bpy.types.Operator): bl_idname = 'psa_import.actions_deselect_all' bl_label = 'None' bl_description = 'Deselect all actions' @@ -320,6 +384,31 @@ class PsaImportDeselectAll(bpy.types.Operator): return {'FINISHED'} +class PSA_PT_ImportPanel_Advanced(Panel): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_label = 'Advanced' + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = 'PSA_PT_ImportPanel' + + def draw(self, context): + layout = self.layout + property_group = context.scene.psa_import + + col = layout.column(heading="Options") + col.use_property_split = True + col.use_property_decorate = False + col.prop(property_group, 'should_clean_keys') + col.separator() + col.prop(property_group, 'should_use_fake_user') + col.prop(property_group, 'should_stash') + col.separator() + col.prop(property_group, 'should_use_action_name_prefix') + + if property_group.should_use_action_name_prefix: + col.prop(property_group, 'action_name_prefix') + + class PSA_PT_ImportPanel(Panel): bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' @@ -336,33 +425,58 @@ class PSA_PT_ImportPanel(Panel): layout = self.layout property_group = context.scene.psa_import - row = layout.row() + row = layout.row(align=True) + row.operator('psa_import.select_file', text='', icon='FILEBROWSER') row.prop(property_group, 'psa_file_path', text='') - row.enabled = False + row.operator('psa_import.file_reload', text='', icon='FILE_REFRESH') - row = layout.row() - row.operator('psa_import.select_file', text='Select PSA File', icon='FILEBROWSER') + if property_group.psa_error != '': + row = layout.row() + row.label(text='File could not be read', icon='ERROR') - if len(property_group.action_list) > 0: - box = layout.box() - box.label(text=f'Actions ({len(property_group.action_list)})', icon='ACTION') - row = box.row() - rows = max(3, min(len(property_group.action_list), 10)) - row.template_list('PSA_UL_ImportActionList', '', property_group, 'action_list', property_group, 'action_list_index', rows=rows) - row = box.row(align=True) - row.label(text='Select') - row.operator('psa_import.actions_select_all', text='All') - row.operator('psa_import.actions_deselect_all', text='None') + box = layout.box() - col = layout.column(heading="Options") - col.use_property_split = True - col.use_property_decorate = False - col.prop(property_group, 'should_clean_keys') - col.prop(property_group, 'should_use_fake_user') - col.prop(property_group, 'should_stash') - col.prop(property_group, 'action_name_prefix') + box.label(text=f'Sequences', icon='ARMATURE_DATA') - layout.operator('psa_import.import', text=f'Import') + # select + rows = max(3, min(len(property_group.sequence_list) + len(property_group.action_list), 10)) + + row = box.row() + col = row.column() + + row2 = col.row(align=True) + row2.label(text='Select') + row2.operator('psa_import.sequences_select_all', text='All') + row2.operator('psa_import.sequences_deselect_all', text='None') + + col = col.row() + col.template_list('PSA_UL_ImportSequenceList', '', property_group, 'sequence_list', property_group, + 'sequence_list_index', rows=rows) + + col = row.column(align=True) + col.operator('psa_import.push_to_actions', icon='TRIA_RIGHT', text='') + col.operator('psa_import.pop_from_actions', icon='TRIA_LEFT', text='') + + col = row.column() + row2 = col.row(align=True) + row2.label(text='Select') + row2.operator('psa_import.actions_select_all', text='All') + row2.operator('psa_import.actions_deselect_all', text='None') + col.template_list('PSA_UL_ImportActionList', '', property_group, 'action_list', property_group, + 'action_list_index', rows=rows) + col.separator() + col.operator('psa_import.import', text=f'Import') + + +class PsaImportFileReload(Operator): + bl_idname = 'psa_import.file_reload' + bl_label = 'Refresh' + bl_options = {'REGISTER'} + bl_description = 'Refresh the PSA file' + + def execute(self, context): + load_psa_file(context) + return {"FINISHED"} class PsaImportSelectFile(Operator): @@ -392,13 +506,12 @@ class PsaImportOperator(Operator): property_group = context.scene.psa_import active_object = context.view_layer.objects.active action_list = property_group.action_list - has_selected_actions = any(map(lambda action: action.is_selected, action_list)) - return has_selected_actions and active_object is not None and active_object.type == 'ARMATURE' + return len(action_list) and active_object is not None and active_object.type == 'ARMATURE' def execute(self, context): property_group = context.scene.psa_import psa_reader = PsaReader(property_group.psa_file_path) - sequence_names = [x.action_name for x in property_group.action_list if x.is_selected] + sequence_names = [x.action_name for x in property_group.action_list] options = PsaImportOptions() options.sequence_names = sequence_names options.should_clean_keys = property_group.should_clean_keys @@ -410,6 +523,52 @@ class PsaImportOperator(Operator): return {'FINISHED'} +class PsaImportPushToActions(Operator): + bl_idname = 'psa_import.push_to_actions' + bl_label = 'Push to Actions' + + @classmethod + def poll(cls, context): + property_group = context.scene.psa_import + has_sequences_selected = any(map(lambda x: x.is_selected, property_group.sequence_list)) + return has_sequences_selected + + def execute(self, context): + property_group = context.scene.psa_import + indices_to_remove = [] + for sequence_index, item in enumerate(property_group.sequence_list): + if item.is_selected: + indices_to_remove.append(sequence_index) + action = property_group.action_list.add() + action.action_name = item.action_name + for index in reversed(indices_to_remove): + property_group.sequence_list.remove(index) + return {'FINISHED'} + + +class PsaImportPopFromActions(Operator): + bl_idname = 'psa_import.pop_from_actions' + bl_label = 'Pop From Actions' + + @classmethod + def poll(cls, context): + property_group = context.scene.psa_import + has_actions_selected = any(map(lambda x: x.is_selected, property_group.action_list)) + return has_actions_selected + + def execute(self, context): + property_group = context.scene.psa_import + indices_to_remove = [] + for action_index, item in enumerate(property_group.action_list): + if item.is_selected: + indices_to_remove.append(action_index) + sequence = property_group.sequence_list.add() + sequence.action_name = item.action_name + for index in reversed(indices_to_remove): + property_group.action_list.remove(index) + return {'FINISHED'} + + class PsaImportFileSelectOperator(Operator, ImportHelper): bl_idname = 'psa_import.file_select' bl_label = 'File Select' @@ -432,15 +591,23 @@ class PsaImportFileSelectOperator(Operator, ImportHelper): return {'FINISHED'} -__classes__ = [ +classes = ( PsaImportPsaBoneItem, PsaImportActionListItem, PsaImportPropertyGroup, + PSA_UL_SequenceList, + PSA_UL_ImportSequenceList, PSA_UL_ImportActionList, - PsaImportSelectAll, - PsaImportDeselectAll, + PsaImportSequencesSelectAll, + PsaImportSequencesDeselectAll, + PsaImportActionsSelectAll, + PsaImportActionsDeselectAll, + PsaImportFileReload, PSA_PT_ImportPanel, + PSA_PT_ImportPanel_Advanced, PsaImportOperator, PsaImportFileSelectOperator, PsaImportSelectFile, -] + PsaImportPushToActions, + PsaImportPopFromActions, +) diff --git a/io_scene_psk_psa/psk/exporter.py b/io_scene_psk_psa/psk/exporter.py index 860c20f..ee0b188 100644 --- a/io_scene_psk_psa/psk/exporter.py +++ b/io_scene_psk_psa/psk/exporter.py @@ -148,7 +148,7 @@ class PskExportPropertyGroup(PropertyGroup): bone_group_list_index: IntProperty(default=0) -__classes__ = [ +classes = ( PskExportOperator, PskExportPropertyGroup -] \ No newline at end of file +) \ No newline at end of file diff --git a/io_scene_psk_psa/psk/importer.py b/io_scene_psk_psa/psk/importer.py index 81283f8..4b593bd 100644 --- a/io_scene_psk_psa/psk/importer.py +++ b/io_scene_psk_psa/psk/importer.py @@ -184,6 +184,6 @@ class PskImportOperator(Operator, ImportHelper): return {'FINISHED'} -__classes__ = [ - PskImportOperator -] +classes = ( + PskImportOperator, +) diff --git a/io_scene_psk_psa/types.py b/io_scene_psk_psa/types.py index 47e6dcd..89b6b26 100644 --- a/io_scene_psk_psa/types.py +++ b/io_scene_psk_psa/types.py @@ -19,7 +19,7 @@ class BoneGroupListItem(PropertyGroup): return self.name -__classes__ = [ +classes = ( BoneGroupListItem, - PSX_UL_BoneGroupList -] + PSX_UL_BoneGroupList, +)