Initial commit for multiple PSA import
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from typing import Dict
|
from typing import Dict, List
|
||||||
|
|
||||||
from .reader import PsaReader
|
from .reader import PsaReader
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ def _get_bone_flags_from_value(value: str) -> int:
|
|||||||
return 0
|
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()
|
psa_config = PsaConfig()
|
||||||
|
|
||||||
config = _load_config_file(file_path)
|
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.
|
# Map the sequence name onto the actual sequence name in the PSA file.
|
||||||
try:
|
try:
|
||||||
psa_sequence_names = list(psa_reader.sequences.keys())
|
|
||||||
lowercase_sequence_names = [sequence_name.lower() for sequence_name in psa_sequence_names]
|
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())]
|
sequence_name = psa_sequence_names[lowercase_sequence_names.index(sequence_name.lower())]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Iterable, List
|
||||||
|
|
||||||
from bpy.props import StringProperty
|
from bpy.props import StringProperty, CollectionProperty
|
||||||
from bpy.types import Operator, Event, Context, FileHandler
|
from bpy.types import Operator, Event, Context, FileHandler, OperatorFileListElement, Object
|
||||||
from bpy_extras.io_utils import ImportHelper
|
from bpy_extras.io_utils import ImportHelper
|
||||||
|
|
||||||
from .properties import get_visible_sequences
|
from .properties import get_visible_sequences
|
||||||
@@ -108,11 +109,92 @@ def load_psa_file(context, filepath: str):
|
|||||||
pg.psa_error = str(e)
|
pg.psa_error = str(e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def on_psa_file_path_updated(cls, context):
|
def on_psa_file_path_updated(cls, context):
|
||||||
load_psa_file(context, cls.filepath)
|
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):
|
class PSA_OT_import(Operator, ImportHelper):
|
||||||
bl_idname = 'psa_import.import'
|
bl_idname = 'psa_import.import'
|
||||||
bl_label = 'Import'
|
bl_label = 'Import'
|
||||||
@@ -138,36 +220,13 @@ class PSA_OT_import(Operator, ImportHelper):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
pg = getattr(context.scene, 'psa_import')
|
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]
|
sequence_names = [x.action_name for x in pg.sequence_list if x.is_selected]
|
||||||
|
|
||||||
if len(sequence_names) == 0:
|
if len(sequence_names) == 0:
|
||||||
self.report({'ERROR_INVALID_CONTEXT'}, 'No sequences selected')
|
self.report({'ERROR_INVALID_CONTEXT'}, 'No sequences selected')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
options = PsaImportOptions()
|
result = _import_psa(context, pg, self.filepath, sequence_names, context.view_layer.objects.active)
|
||||||
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)
|
|
||||||
|
|
||||||
if len(result.warnings) > 0:
|
if len(result.warnings) > 0:
|
||||||
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
|
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):
|
def draw(self, context: Context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
pg = getattr(context.scene, 'psa_import')
|
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:
|
def draw_psa_import_options_no_panels(layout, pg):
|
||||||
if pg.psa_error:
|
col = layout.column(heading='Sequences')
|
||||||
row = sequences_panel.row()
|
col.use_property_split = True
|
||||||
row.label(text='Select a PSA file', icon='ERROR')
|
col.use_property_decorate = False
|
||||||
else:
|
col.prop(pg, 'fps_source')
|
||||||
# Select buttons.
|
if pg.fps_source == 'CUSTOM':
|
||||||
rows = max(3, min(len(pg.sequence_list), 10))
|
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 = layout.column(heading='Write')
|
||||||
col = row.column()
|
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)
|
if pg.should_write_keyframes:
|
||||||
row2.label(text='Select')
|
col = col.column(heading='Keyframes')
|
||||||
row2.operator(PSA_OT_import_sequences_from_text.bl_idname, text='', icon='TEXT')
|
col.use_property_split = True
|
||||||
row2.operator(PSA_OT_import_sequences_select_all.bl_idname, text='All', icon='CHECKBOX_HLT')
|
col.use_property_decorate = False
|
||||||
row2.operator(PSA_OT_import_sequences_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT')
|
col.prop(pg, 'should_convert_to_samples')
|
||||||
|
|
||||||
col = col.row()
|
col = layout.column()
|
||||||
col.template_list('PSA_UL_import_sequences', '', pg, 'sequence_list', pg, 'sequence_list_index', rows=rows)
|
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_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(pg, 'fps_source')
|
col.prop(pg, 'should_convert_to_samples')
|
||||||
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)
|
advanced_header, advanced_panel = layout.panel('advanced_panel_id', default_closed=True)
|
||||||
data_header.label(text='Data')
|
advanced_header.label(text='Advanced')
|
||||||
|
|
||||||
if data_panel:
|
if advanced_panel:
|
||||||
col = data_panel.column(heading='Write')
|
col = advanced_panel.column()
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(pg, 'should_write_keyframes')
|
col.prop(pg, 'bone_mapping_mode')
|
||||||
col.prop(pg, 'should_write_metadata')
|
|
||||||
|
|
||||||
if pg.should_write_keyframes:
|
col = advanced_panel.column(heading='Options')
|
||||||
col = col.column(heading='Keyframes')
|
col.use_property_split = True
|
||||||
col.use_property_split = True
|
col.use_property_decorate = False
|
||||||
col.use_property_decorate = False
|
col.prop(pg, 'should_use_fake_user')
|
||||||
col.prop(pg, 'should_convert_to_samples')
|
col.prop(pg, 'should_stash')
|
||||||
|
col.prop(pg, 'should_use_config_file')
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
class PSA_FH_import(FileHandler):
|
class PSA_FH_import(FileHandler):
|
||||||
bl_idname = 'PSA_FH_import'
|
bl_idname = 'PSA_FH_import'
|
||||||
bl_label = 'File handler for Unreal PSA 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'
|
bl_file_extensions = '.psa'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -273,5 +372,6 @@ classes = (
|
|||||||
PSA_OT_import_sequences_deselect_all,
|
PSA_OT_import_sequences_deselect_all,
|
||||||
PSA_OT_import_sequences_from_text,
|
PSA_OT_import_sequences_from_text,
|
||||||
PSA_OT_import,
|
PSA_OT_import,
|
||||||
|
PSA_OT_import_multiple,
|
||||||
PSA_FH_import,
|
PSA_FH_import,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ def _get_sample_frame_times(source_frame_count: int, frame_step: float) -> typin
|
|||||||
time += frame_step
|
time += frame_step
|
||||||
yield source_frame_count - 1
|
yield source_frame_count - 1
|
||||||
|
|
||||||
|
|
||||||
def _resample_sequence_data_matrix(sequence_data_matrix: np.ndarray, frame_step: float = 1.0) -> np.ndarray:
|
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.
|
Resamples the sequence data matrix to the target frame count.
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ class PSK_OT_import(Operator, ImportHelper):
|
|||||||
name='File Path',
|
name='File Path',
|
||||||
description='File path used for exporting the PSK file',
|
description='File path used for exporting the PSK file',
|
||||||
maxlen=1024,
|
maxlen=1024,
|
||||||
default='')
|
subtype='FILE_PATH',
|
||||||
|
options={'SKIP_SAVE'})
|
||||||
|
|
||||||
should_import_vertex_colors: BoolProperty(
|
should_import_vertex_colors: BoolProperty(
|
||||||
default=True,
|
default=True,
|
||||||
|
|||||||
Reference in New Issue
Block a user