Compare commits

..

3 Commits

Author SHA1 Message Date
Colin Basnett
7ceaa88f1d Incremented version to 7.0.1 2024-03-31 14:23:35 -07:00
Colin Basnett
37e246bf3e Fixed ordering of panels in the PSA import dialog 2024-03-31 14:22:16 -07:00
Colin Basnett
db93314fbc Initial commit for multiple PSA import 2024-03-31 12:47:48 -07:00
5 changed files with 189 additions and 88 deletions

View File

@@ -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': '',

View File

@@ -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:

View File

@@ -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('00_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('01_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('02_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,
)

View File

@@ -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.

View File

@@ -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,