From db93314fbca7065795d9880cc7053a7c521b7fe4 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Sun, 31 Mar 2024 12:47:48 -0700 Subject: [PATCH 1/3] Initial commit for multiple PSA import --- io_scene_psk_psa/psa/config.py | 5 +- io_scene_psk_psa/psa/import_/operators.py | 266 +++++++++++++++------- io_scene_psk_psa/psa/importer.py | 1 + io_scene_psk_psa/psk/import_/operators.py | 3 +- 4 files changed, 188 insertions(+), 87 deletions(-) diff --git a/io_scene_psk_psa/psa/config.py b/io_scene_psk_psa/psa/config.py index 66864e4..0914134 100644 --- a/io_scene_psk_psa/psa/config.py +++ b/io_scene_psk_psa/psa/config.py @@ -1,6 +1,6 @@ import re from configparser import ConfigParser -from typing import Dict +from typing import Dict, List from .reader import PsaReader @@ -50,7 +50,7 @@ def _get_bone_flags_from_value(value: str) -> int: return 0 -def read_psa_config(psa_reader: PsaReader, file_path: str) -> PsaConfig: +def read_psa_config(psa_sequence_names: List[str], file_path: str) -> PsaConfig: psa_config = PsaConfig() config = _load_config_file(file_path) @@ -62,7 +62,6 @@ def read_psa_config(psa_reader: PsaReader, file_path: str) -> PsaConfig: # Map the sequence name onto the actual sequence name in the PSA file. try: - psa_sequence_names = list(psa_reader.sequences.keys()) lowercase_sequence_names = [sequence_name.lower() for sequence_name in psa_sequence_names] sequence_name = psa_sequence_names[lowercase_sequence_names.index(sequence_name.lower())] except ValueError: diff --git a/io_scene_psk_psa/psa/import_/operators.py b/io_scene_psk_psa/psa/import_/operators.py index 0dd98c9..5e2537e 100644 --- a/io_scene_psk_psa/psa/import_/operators.py +++ b/io_scene_psk_psa/psa/import_/operators.py @@ -1,8 +1,9 @@ import os from pathlib import Path +from typing import Iterable, List -from bpy.props import StringProperty -from bpy.types import Operator, Event, Context, FileHandler +from bpy.props import StringProperty, CollectionProperty +from bpy.types import Operator, Event, Context, FileHandler, OperatorFileListElement, Object from bpy_extras.io_utils import ImportHelper from .properties import get_visible_sequences @@ -108,11 +109,92 @@ def load_psa_file(context, filepath: str): pg.psa_error = str(e) - def on_psa_file_path_updated(cls, context): load_psa_file(context, cls.filepath) +class PSA_OT_import_multiple(Operator): + bl_idname = 'psa_import.import_multiple' + bl_label = 'Import PSA' + bl_description = 'Import multiple PSA files' + bl_options = {'INTERNAL', 'UNDO'} + + directory: StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE', 'HIDDEN'}) + files: CollectionProperty(type=OperatorFileListElement, options={'SKIP_SAVE', 'HIDDEN'}) + + @classmethod + def poll(cls, context): + return True + + def execute(self, context): + pg = getattr(context.scene, 'psa_import') + warnings = [] + + for file in self.files: + psa_path = os.path.join(self.directory, file.name) + psa_reader = PsaReader(psa_path) + sequence_names = psa_reader.sequences.keys() + + result = _import_psa(context, pg, psa_path, sequence_names, context.view_layer.objects.active) + result.warnings.extend(warnings) + + if len(result.warnings) > 0: + message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n' + self.report({'INFO'}, message) + for warning in result.warnings: + self.report({'WARNING'}, warning) + + return {'FINISHED'} + + def invoke(self, context: Context, event): + # Show the import operator properties in a pop-up dialog (do not use the file selector). + context.window_manager.invoke_props_dialog(self) + return {'RUNNING_MODAL'} + + def draw(self, context): + layout = self.layout + pg = getattr(context.scene, 'psa_import') + draw_psa_import_options_no_panels(layout, pg) + + +def _import_psa(context, + pg, + filepath: str, + sequence_names: List[str], + armature_object: Object + ): + options = PsaImportOptions() + options.sequence_names = sequence_names + options.should_use_fake_user = pg.should_use_fake_user + options.should_stash = pg.should_stash + options.action_name_prefix = pg.action_name_prefix if pg.should_use_action_name_prefix else '' + options.should_overwrite = pg.should_overwrite + options.should_write_metadata = pg.should_write_metadata + options.should_write_keyframes = pg.should_write_keyframes + options.should_convert_to_samples = pg.should_convert_to_samples + options.bone_mapping_mode = pg.bone_mapping_mode + options.fps_source = pg.fps_source + options.fps_custom = pg.fps_custom + + warnings = [] + + if options.should_use_config_file: + # Read the PSA config file if it exists. + config_path = Path(filepath).with_suffix('.config') + if config_path.exists(): + try: + options.psa_config = read_psa_config(sequence_names, str(config_path)) + except Exception as e: + warnings.append(f'Failed to read PSA config file: {e}') + + psa_reader = PsaReader(filepath) + + result = import_psa(context, psa_reader, armature_object, options) + result.warnings.extend(warnings) + + return result + + class PSA_OT_import(Operator, ImportHelper): bl_idname = 'psa_import.import' bl_label = 'Import' @@ -138,36 +220,13 @@ class PSA_OT_import(Operator, ImportHelper): def execute(self, context): pg = getattr(context.scene, 'psa_import') - psa_reader = PsaReader(self.filepath) sequence_names = [x.action_name for x in pg.sequence_list if x.is_selected] if len(sequence_names) == 0: self.report({'ERROR_INVALID_CONTEXT'}, 'No sequences selected') return {'CANCELLED'} - options = PsaImportOptions() - options.sequence_names = sequence_names - options.should_use_fake_user = pg.should_use_fake_user - options.should_stash = pg.should_stash - options.action_name_prefix = pg.action_name_prefix if pg.should_use_action_name_prefix else '' - options.should_overwrite = pg.should_overwrite - options.should_write_metadata = pg.should_write_metadata - options.should_write_keyframes = pg.should_write_keyframes - options.should_convert_to_samples = pg.should_convert_to_samples - options.bone_mapping_mode = pg.bone_mapping_mode - options.fps_source = pg.fps_source - options.fps_custom = pg.fps_custom - - if options.should_use_config_file: - # Read the PSA config file if it exists. - config_path = Path(self.filepath).with_suffix('.config') - if config_path.exists(): - try: - options.psa_config = read_psa_config(psa_reader, str(config_path)) - except Exception as e: - self.report({'WARNING'}, f'Failed to read PSA config file: {e}') - - result = import_psa(context, psa_reader, context.view_layer.objects.active, options) + result = _import_psa(context, pg, self.filepath, sequence_names, context.view_layer.objects.active) if len(result.warnings) > 0: message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n' @@ -189,78 +248,118 @@ class PSA_OT_import(Operator, ImportHelper): def draw(self, context: Context): layout = self.layout pg = getattr(context.scene, 'psa_import') + draw_psa_import_options(layout, pg) - sequences_header, sequences_panel = layout.panel('sequences_panel_id', default_closed=False) - sequences_header.label(text='Sequences') - if sequences_panel: - if pg.psa_error: - row = sequences_panel.row() - row.label(text='Select a PSA file', icon='ERROR') - else: - # Select buttons. - rows = max(3, min(len(pg.sequence_list), 10)) +def draw_psa_import_options_no_panels(layout, pg): + col = layout.column(heading='Sequences') + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'fps_source') + if pg.fps_source == 'CUSTOM': + col.prop(pg, 'fps_custom') + col.prop(pg, 'should_overwrite') + col.prop(pg, 'should_use_action_name_prefix') + if pg.should_use_action_name_prefix: + col.prop(pg, 'action_name_prefix') - row = sequences_panel.row() - col = row.column() + col = layout.column(heading='Write') + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'should_write_keyframes') + col.prop(pg, 'should_write_metadata') - row2 = col.row(align=True) - row2.label(text='Select') - row2.operator(PSA_OT_import_sequences_from_text.bl_idname, text='', icon='TEXT') - row2.operator(PSA_OT_import_sequences_select_all.bl_idname, text='All', icon='CHECKBOX_HLT') - row2.operator(PSA_OT_import_sequences_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT') + if pg.should_write_keyframes: + col = col.column(heading='Keyframes') + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'should_convert_to_samples') - col = col.row() - col.template_list('PSA_UL_import_sequences', '', pg, 'sequence_list', pg, 'sequence_list_index', rows=rows) + col = layout.column() + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'bone_mapping_mode') - col = sequences_panel.column(heading='') + col = layout.column(heading='Options') + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'should_use_fake_user') + col.prop(pg, 'should_stash') + col.prop(pg, 'should_use_config_file') + + +def draw_psa_import_options(layout, pg): + sequences_header, sequences_panel = layout.panel('sequences_panel_id', default_closed=False) + sequences_header.label(text='Sequences') + + if sequences_panel: + if pg.psa_error: + row = sequences_panel.row() + row.label(text='Select a PSA file', icon='ERROR') + else: + # Select buttons. + rows = max(3, min(len(pg.sequence_list), 10)) + + row = sequences_panel.row() + col = row.column() + + row2 = col.row(align=True) + row2.label(text='Select') + row2.operator(PSA_OT_import_sequences_from_text.bl_idname, text='', icon='TEXT') + row2.operator(PSA_OT_import_sequences_select_all.bl_idname, text='All', icon='CHECKBOX_HLT') + row2.operator(PSA_OT_import_sequences_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT') + + col = col.row() + col.template_list('PSA_UL_import_sequences', '', pg, 'sequence_list', pg, 'sequence_list_index', rows=rows) + + col = sequences_panel.column(heading='') + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'fps_source') + if pg.fps_source == 'CUSTOM': + col.prop(pg, 'fps_custom') + col.prop(pg, 'should_overwrite') + col.prop(pg, 'should_use_action_name_prefix') + if pg.should_use_action_name_prefix: + col.prop(pg, 'action_name_prefix') + + data_header, data_panel = layout.panel('data_panel_id', default_closed=False) + data_header.label(text='Data') + + if data_panel: + col = data_panel.column(heading='Write') + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'should_write_keyframes') + col.prop(pg, 'should_write_metadata') + + if pg.should_write_keyframes: + col = col.column(heading='Keyframes') col.use_property_split = True col.use_property_decorate = False - col.prop(pg, 'fps_source') - if pg.fps_source == 'CUSTOM': - col.prop(pg, 'fps_custom') - col.prop(pg, 'should_overwrite') - col.prop(pg, 'should_use_action_name_prefix') - if pg.should_use_action_name_prefix: - col.prop(pg, 'action_name_prefix') + col.prop(pg, 'should_convert_to_samples') - data_header, data_panel = layout.panel('data_panel_id', default_closed=False) - data_header.label(text='Data') + advanced_header, advanced_panel = layout.panel('advanced_panel_id', default_closed=True) + advanced_header.label(text='Advanced') - if data_panel: - col = data_panel.column(heading='Write') - col.use_property_split = True - col.use_property_decorate = False - col.prop(pg, 'should_write_keyframes') - col.prop(pg, 'should_write_metadata') + if advanced_panel: + col = advanced_panel.column() + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'bone_mapping_mode') - if pg.should_write_keyframes: - col = col.column(heading='Keyframes') - col.use_property_split = True - col.use_property_decorate = False - col.prop(pg, 'should_convert_to_samples') - - advanced_header, advanced_panel = layout.panel('advanced_panel_id', default_closed=True) - advanced_header.label(text='Advanced') - - if advanced_panel: - col = advanced_panel.column() - col.use_property_split = True - col.use_property_decorate = False - col.prop(pg, 'bone_mapping_mode') - - col = advanced_panel.column(heading='Options') - col.use_property_split = True - col.use_property_decorate = False - col.prop(pg, 'should_use_fake_user') - col.prop(pg, 'should_stash') - col.prop(pg, 'should_use_config_file') + col = advanced_panel.column(heading='Options') + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'should_use_fake_user') + col.prop(pg, 'should_stash') + col.prop(pg, 'should_use_config_file') class PSA_FH_import(FileHandler): bl_idname = 'PSA_FH_import' bl_label = 'File handler for Unreal PSA import' - bl_import_operator = 'psa_import.import' + bl_import_operator = 'psa_import.import_multiple' bl_file_extensions = '.psa' @classmethod @@ -273,5 +372,6 @@ classes = ( PSA_OT_import_sequences_deselect_all, PSA_OT_import_sequences_from_text, PSA_OT_import, + PSA_OT_import_multiple, PSA_FH_import, ) diff --git a/io_scene_psk_psa/psa/importer.py b/io_scene_psk_psa/psa/importer.py index 3d20cb7..37dc818 100644 --- a/io_scene_psk_psa/psa/importer.py +++ b/io_scene_psk_psa/psa/importer.py @@ -88,6 +88,7 @@ def _get_sample_frame_times(source_frame_count: int, frame_step: float) -> typin time += frame_step yield source_frame_count - 1 + def _resample_sequence_data_matrix(sequence_data_matrix: np.ndarray, frame_step: float = 1.0) -> np.ndarray: """ Resamples the sequence data matrix to the target frame count. diff --git a/io_scene_psk_psa/psk/import_/operators.py b/io_scene_psk_psa/psk/import_/operators.py index 8d9dad2..6954f58 100644 --- a/io_scene_psk_psa/psk/import_/operators.py +++ b/io_scene_psk_psa/psk/import_/operators.py @@ -33,7 +33,8 @@ class PSK_OT_import(Operator, ImportHelper): name='File Path', description='File path used for exporting the PSK file', maxlen=1024, - default='') + subtype='FILE_PATH', + options={'SKIP_SAVE'}) should_import_vertex_colors: BoolProperty( default=True, From 37e246bf3e535b80e6f91a7c4bb754c11325b1a1 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Sun, 31 Mar 2024 14:22:16 -0700 Subject: [PATCH 2/3] Fixed ordering of panels in the PSA import dialog --- io_scene_psk_psa/psa/import_/operators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/io_scene_psk_psa/psa/import_/operators.py b/io_scene_psk_psa/psa/import_/operators.py index 5e2537e..d572d89 100644 --- a/io_scene_psk_psa/psa/import_/operators.py +++ b/io_scene_psk_psa/psa/import_/operators.py @@ -289,7 +289,7 @@ def draw_psa_import_options_no_panels(layout, pg): def draw_psa_import_options(layout, pg): - sequences_header, sequences_panel = layout.panel('sequences_panel_id', default_closed=False) + sequences_header, sequences_panel = layout.panel('00_sequences_panel_id', default_closed=False) sequences_header.label(text='Sequences') if sequences_panel: @@ -323,7 +323,7 @@ def draw_psa_import_options(layout, pg): if pg.should_use_action_name_prefix: col.prop(pg, 'action_name_prefix') - data_header, data_panel = layout.panel('data_panel_id', default_closed=False) + data_header, data_panel = layout.panel('01_data_panel_id', default_closed=False) data_header.label(text='Data') if data_panel: @@ -339,7 +339,7 @@ def draw_psa_import_options(layout, pg): col.use_property_decorate = False col.prop(pg, 'should_convert_to_samples') - advanced_header, advanced_panel = layout.panel('advanced_panel_id', default_closed=True) + advanced_header, advanced_panel = layout.panel('02_advanced_panel_id', default_closed=True) advanced_header.label(text='Advanced') if advanced_panel: From 7ceaa88f1d9d729a3f7d77a3e2e888e3cd8fcb62 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Sun, 31 Mar 2024 14:23:35 -0700 Subject: [PATCH 3/3] Incremented version to 7.0.1 --- io_scene_psk_psa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_psk_psa/__init__.py b/io_scene_psk_psa/__init__.py index ab2587b..7419919 100644 --- a/io_scene_psk_psa/__init__.py +++ b/io_scene_psk_psa/__init__.py @@ -3,7 +3,7 @@ from bpy.app.handlers import persistent bl_info = { 'name': 'PSK/PSA Importer/Exporter', 'author': 'Colin Basnett, Yurii Ti', - 'version': (7, 0, 0), + 'version': (7, 0, 1), 'blender': (4, 1, 0), 'description': 'PSK/PSA Import/Export (.psk/.psa)', 'warning': '',