The PSA import functionality has been moved to a file import dialog.

As a result, the "PSA Import" panel in the Armature Data tab has been
removed as it is now redundant.

This was made possible by https://developer.blender.org/D15543.

As a result, the minimum Blender version has now been bumped to 3.4.

The 4.2.0 version is now in LTS mode and will not be receiving new
features.
This commit is contained in:
Colin Basnett
2023-01-02 15:38:43 -08:00
parent 17e9e83826
commit b26e49d403
2 changed files with 83 additions and 144 deletions

View File

@@ -1,9 +1,8 @@
bl_info = {
"name": "PSK/PSA Importer/Exporter",
"author": "Colin Basnett, Yurii Ti",
"version": (4, 2, 0),
"blender": (2, 80, 0),
# "location": "File > Export > PSK Export (.psk)",
"version": (5, 0, 0),
"blender": (3, 4, 0),
"description": "PSK/PSA Import/Export (.psk/.psa)",
"warning": "",
"doc_url": "https://github.com/DarklightGames/io_scene_psk_psa",
@@ -71,6 +70,7 @@ def register():
bpy.types.TOPBAR_MT_file_export.append(psk_export_menu_func)
bpy.types.TOPBAR_MT_file_import.append(psk_import_menu_func)
bpy.types.TOPBAR_MT_file_export.append(psa_export_menu_func)
bpy.types.TOPBAR_MT_file_import.append(psa_import_menu_func)
bpy.types.Scene.psa_import = PointerProperty(type=psa_importer.PsaImportPropertyGroup)
bpy.types.Scene.psk_import = PointerProperty(type=psk_importer.PskImportPropertyGroup)
bpy.types.Scene.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup)
@@ -85,6 +85,7 @@ def unregister():
bpy.types.TOPBAR_MT_file_export.remove(psk_export_menu_func)
bpy.types.TOPBAR_MT_file_import.remove(psk_import_menu_func)
bpy.types.TOPBAR_MT_file_export.remove(psa_export_menu_func)
bpy.types.TOPBAR_MT_file_import.remove(psa_import_menu_func)
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@@ -8,7 +8,7 @@ from typing import List, Optional
import bpy
import numpy
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty, EnumProperty
from bpy.types import Operator, UIList, PropertyGroup, Panel, FCurve
from bpy.types import Operator, UIList, PropertyGroup, FCurve
from bpy_extras.io_utils import ImportHelper
from mathutils import Vector, Quaternion
@@ -243,14 +243,14 @@ class PsaImportActionListItem(PropertyGroup):
is_selected: BoolProperty(default=False, options=empty_set)
def load_psa_file(context):
def load_psa_file(context, filepath: str):
pg = context.scene.psa_import
pg.sequence_list.clear()
pg.psa.bones.clear()
pg.psa_error = ''
try:
# Read the file and populate the action list.
p = os.path.abspath(pg.psa_file_path)
p = os.path.abspath(filepath)
psa_reader = PsaReader(p)
for sequence in psa_reader.sequences.values():
item = pg.sequence_list.add()
@@ -262,8 +262,8 @@ def load_psa_file(context):
pg.psa_error = str(e)
def on_psa_file_path_updated(property_, context):
load_psa_file(context)
def on_psa_file_path_updated(cls, context):
load_psa_file(context, cls.filepath)
class PsaBonePropertyGroup(PropertyGroup):
@@ -276,7 +276,6 @@ class PsaDataPropertyGroup(PropertyGroup):
class PsaImportPropertyGroup(PropertyGroup):
psa_file_path: StringProperty(default='', options=empty_set, update=on_psa_file_path_updated, name='PSA File Path')
psa_error: StringProperty(default='')
psa: PointerProperty(type=PsaDataPropertyGroup)
sequence_list: CollectionProperty(type=PsaImportActionListItem)
@@ -477,70 +476,93 @@ class PsaImportSequencesDeselectAll(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'
class PsaImportSelectFile(Operator):
bl_idname = 'psa_import.select_file'
bl_label = 'Select'
bl_options = {'INTERNAL'}
bl_description = 'Select a PSA file from which to import animations'
filepath: bpy.props.StringProperty(subtype='FILE_PATH')
filter_glob: bpy.props.StringProperty(default="*.psa", options={'HIDDEN'})
def draw(self, context):
layout = self.layout
pg = getattr(context.scene, 'psa_import')
def execute(self, context):
getattr(context.scene, 'psa_import').psa_file_path = self.filepath
return {"FINISHED"}
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'bone_mapping_mode')
if pg.should_write_keyframes:
col = layout.column(heading='Keyframes')
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'should_convert_to_samples')
col.separator()
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_action_name_prefix')
if pg.should_use_action_name_prefix:
col.prop(pg, 'action_name_prefix')
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class PSA_PT_ImportPanel(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_label = 'PSA Import'
bl_context = 'data'
bl_category = 'PSA Import'
bl_options = {'DEFAULT_CLOSED'}
class PsaImportOperator(Operator, ImportHelper):
bl_idname = 'psa_import.import'
bl_label = 'Import'
bl_description = 'Import the selected animations into the scene as actions'
bl_options = {'INTERNAL', 'UNDO'}
filename_ext = '.psa'
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
filepath: StringProperty(
name='File Path',
description='File path used for importing the PSA file',
maxlen=1024,
default='',
update=on_psa_file_path_updated)
@classmethod
def poll(cls, context):
return context.view_layer.objects.active.type == 'ARMATURE'
active_object = context.view_layer.objects.active
if active_object is None or active_object.type != 'ARMATURE':
cls.poll_message_set('The active object must be an armature')
return False
return True
def draw(self, context):
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]
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
result = import_psa(psa_reader, context.view_layer.objects.active, options)
if len(result.warnings) > 0:
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
message += '\n'.join(result.warnings)
self.report({'WARNING'}, message)
else:
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
return {'FINISHED'}
def invoke(self, context: bpy.types.Context, event: bpy.types.Event):
# Attempt to load the PSA file for the pre-selected file.
load_psa_file(context, self.filepath)
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def draw(self, context: bpy.types.Context):
layout = self.layout
pg = getattr(context.scene, 'psa_import')
row = layout.row(align=True)
row.operator(PsaImportSelectFile.bl_idname, text='', icon='FILEBROWSER')
row.prop(pg, 'psa_file_path', text='')
row.operator(PsaImportFileReload.bl_idname, text='', icon='FILE_REFRESH')
if pg.psa_error != '':
if pg.psa_error:
row = layout.row()
row.label(text='File could not be read', icon='ERROR')
row.label(text='Select a PSA file', icon='ERROR')
else:
box = layout.box()
box.label(text=f'Sequences ({len(pg.sequence_list)})', icon='ARMATURE_DATA')
# select
# Select buttons.
rows = max(3, min(len(pg.sequence_list), 10))
row = box.row()
@@ -566,107 +588,27 @@ class PSA_PT_ImportPanel(Panel):
col.prop(pg, 'should_write_keyframes')
col.prop(pg, 'should_write_metadata')
selected_sequence_count = sum(map(lambda x: x.is_selected, pg.sequence_list))
col = layout.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'bone_mapping_mode')
row = layout.row()
if pg.should_write_keyframes:
col = layout.column(heading='Keyframes')
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'should_convert_to_samples')
col.separator()
import_button_text = 'Import'
if selected_sequence_count > 0:
import_button_text = f'Import ({selected_sequence_count})'
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_action_name_prefix')
row.operator(PsaImportOperator.bl_idname, text=import_button_text)
class PsaImportFileReload(Operator):
bl_idname = 'psa_import.file_reload'
bl_label = 'Refresh'
bl_options = {'INTERNAL'}
bl_description = 'Refresh the PSA file'
def execute(self, context):
load_psa_file(context)
return {"FINISHED"}
class PsaImportSelectFile(Operator):
bl_idname = 'psa_import.select_file'
bl_label = 'Select'
bl_options = {'INTERNAL'}
bl_description = 'Select a PSA file from which to import animations'
filepath: bpy.props.StringProperty(subtype='FILE_PATH')
filter_glob: bpy.props.StringProperty(default="*.psa", options={'HIDDEN'})
def execute(self, context):
getattr(context.scene, 'psa_import').psa_file_path = self.filepath
return {"FINISHED"}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"}
class PsaImportOperator(Operator):
bl_idname = 'psa_import.import'
bl_label = 'Import'
bl_description = 'Import the selected animations into the scene as actions'
bl_options = {'INTERNAL', 'UNDO'}
@classmethod
def poll(cls, context):
pg = getattr(context.scene, 'psa_import')
active_object = context.view_layer.objects.active
if active_object is None or active_object.type != 'ARMATURE':
return False
return any(map(lambda x: x.is_selected, pg.sequence_list))
def execute(self, context):
pg = getattr(context.scene, 'psa_import')
psa_reader = PsaReader(pg.psa_file_path)
sequence_names = [x.action_name for x in pg.sequence_list if x.is_selected]
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
result = import_psa(psa_reader, context.view_layer.objects.active, options)
if len(result.warnings) > 0:
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
message += '\n'.join(result.warnings)
self.report({'WARNING'}, message)
else:
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
return {'FINISHED'}
class PsaImportFileSelectOperator(Operator, ImportHelper):
bl_idname = 'psa_import.file_select'
bl_label = 'File Select'
bl_options = {'INTERNAL'}
filename_ext = '.psa'
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
filepath: StringProperty(
name='File Path',
description='File path used for importing the PSA file',
maxlen=1024,
default='')
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
pg = getattr(context.scene, 'psa_import')
pg.psa_file_path = self.filepath
return {'FINISHED'}
if pg.should_use_action_name_prefix:
col.prop(pg, 'action_name_prefix')
classes = (
@@ -680,10 +622,6 @@ classes = (
PsaImportSequencesSelectAll,
PsaImportSequencesDeselectAll,
PsaImportSequencesFromText,
PsaImportFileReload,
PSA_PT_ImportPanel,
PSA_PT_ImportPanel_Advanced,
PsaImportOperator,
PsaImportFileSelectOperator,
PsaImportSelectFile,
)