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 = { bl_info = {
"name": "PSK/PSA Importer/Exporter", "name": "PSK/PSA Importer/Exporter",
"author": "Colin Basnett, Yurii Ti", "author": "Colin Basnett, Yurii Ti",
"version": (4, 2, 0), "version": (5, 0, 0),
"blender": (2, 80, 0), "blender": (3, 4, 0),
# "location": "File > Export > PSK Export (.psk)",
"description": "PSK/PSA Import/Export (.psk/.psa)", "description": "PSK/PSA Import/Export (.psk/.psa)",
"warning": "", "warning": "",
"doc_url": "https://github.com/DarklightGames/io_scene_psk_psa", "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_export.append(psk_export_menu_func)
bpy.types.TOPBAR_MT_file_import.append(psk_import_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_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.psa_import = PointerProperty(type=psa_importer.PsaImportPropertyGroup)
bpy.types.Scene.psk_import = PointerProperty(type=psk_importer.PskImportPropertyGroup) bpy.types.Scene.psk_import = PointerProperty(type=psk_importer.PskImportPropertyGroup)
bpy.types.Scene.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup) 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_export.remove(psk_export_menu_func)
bpy.types.TOPBAR_MT_file_import.remove(psk_import_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_export.remove(psa_export_menu_func)
bpy.types.TOPBAR_MT_file_import.remove(psa_import_menu_func)
for cls in reversed(classes): for cls in reversed(classes):
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)

View File

@@ -8,7 +8,7 @@ from typing import List, Optional
import bpy import bpy
import numpy import numpy
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty, EnumProperty 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 bpy_extras.io_utils import ImportHelper
from mathutils import Vector, Quaternion from mathutils import Vector, Quaternion
@@ -243,14 +243,14 @@ class PsaImportActionListItem(PropertyGroup):
is_selected: BoolProperty(default=False, options=empty_set) 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 = context.scene.psa_import
pg.sequence_list.clear() pg.sequence_list.clear()
pg.psa.bones.clear() pg.psa.bones.clear()
pg.psa_error = '' pg.psa_error = ''
try: try:
# Read the file and populate the action list. # 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) psa_reader = PsaReader(p)
for sequence in psa_reader.sequences.values(): for sequence in psa_reader.sequences.values():
item = pg.sequence_list.add() item = pg.sequence_list.add()
@@ -262,8 +262,8 @@ def load_psa_file(context):
pg.psa_error = str(e) pg.psa_error = str(e)
def on_psa_file_path_updated(property_, context): def on_psa_file_path_updated(cls, context):
load_psa_file(context) load_psa_file(context, cls.filepath)
class PsaBonePropertyGroup(PropertyGroup): class PsaBonePropertyGroup(PropertyGroup):
@@ -276,7 +276,6 @@ class PsaDataPropertyGroup(PropertyGroup):
class PsaImportPropertyGroup(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_error: StringProperty(default='')
psa: PointerProperty(type=PsaDataPropertyGroup) psa: PointerProperty(type=PsaDataPropertyGroup)
sequence_list: CollectionProperty(type=PsaImportActionListItem) sequence_list: CollectionProperty(type=PsaImportActionListItem)
@@ -477,70 +476,93 @@ class PsaImportSequencesDeselectAll(Operator):
return {'FINISHED'} return {'FINISHED'}
class PSA_PT_ImportPanel_Advanced(Panel): class PsaImportSelectFile(Operator):
bl_space_type = 'PROPERTIES' bl_idname = 'psa_import.select_file'
bl_region_type = 'WINDOW' bl_label = 'Select'
bl_label = 'Advanced' bl_options = {'INTERNAL'}
bl_options = {'DEFAULT_CLOSED'} bl_description = 'Select a PSA file from which to import animations'
bl_parent_id = 'PSA_PT_ImportPanel' filepath: bpy.props.StringProperty(subtype='FILE_PATH')
filter_glob: bpy.props.StringProperty(default="*.psa", options={'HIDDEN'})
def draw(self, context): def execute(self, context):
layout = self.layout getattr(context.scene, 'psa_import').psa_file_path = self.filepath
pg = getattr(context.scene, 'psa_import') return {"FINISHED"}
col = layout.column() def invoke(self, context, event):
col.use_property_split = True context.window_manager.fileselect_add(self)
col.use_property_decorate = False return {"RUNNING_MODAL"}
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')
class PSA_PT_ImportPanel(Panel): class PsaImportOperator(Operator, ImportHelper):
bl_space_type = 'PROPERTIES' bl_idname = 'psa_import.import'
bl_region_type = 'WINDOW' bl_label = 'Import'
bl_label = 'PSA Import' bl_description = 'Import the selected animations into the scene as actions'
bl_context = 'data' bl_options = {'INTERNAL', 'UNDO'}
bl_category = 'PSA Import'
bl_options = {'DEFAULT_CLOSED'} 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 @classmethod
def poll(cls, context): 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 layout = self.layout
pg = getattr(context.scene, 'psa_import') pg = getattr(context.scene, 'psa_import')
row = layout.row(align=True) if pg.psa_error:
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 != '':
row = layout.row() 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 = layout.box()
box.label(text=f'Sequences ({len(pg.sequence_list)})', icon='ARMATURE_DATA') box.label(text=f'Sequences ({len(pg.sequence_list)})', icon='ARMATURE_DATA')
# select # Select buttons.
rows = max(3, min(len(pg.sequence_list), 10)) rows = max(3, min(len(pg.sequence_list), 10))
row = box.row() row = box.row()
@@ -566,107 +588,27 @@ class PSA_PT_ImportPanel(Panel):
col.prop(pg, 'should_write_keyframes') col.prop(pg, 'should_write_keyframes')
col.prop(pg, 'should_write_metadata') 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' col = layout.column(heading='Options')
if selected_sequence_count > 0: col.use_property_split = True
import_button_text = f'Import ({selected_sequence_count})' 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) if pg.should_use_action_name_prefix:
col.prop(pg, 'action_name_prefix')
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'}
classes = ( classes = (
@@ -680,10 +622,6 @@ classes = (
PsaImportSequencesSelectAll, PsaImportSequencesSelectAll,
PsaImportSequencesDeselectAll, PsaImportSequencesDeselectAll,
PsaImportSequencesFromText, PsaImportSequencesFromText,
PsaImportFileReload,
PSA_PT_ImportPanel,
PSA_PT_ImportPanel_Advanced,
PsaImportOperator, PsaImportOperator,
PsaImportFileSelectOperator,
PsaImportSelectFile, PsaImportSelectFile,
) )