* Considerable clean-up of existing code.

* Moved the PSA import to the properties panel.
This commit is contained in:
Colin Basnett
2022-01-22 21:44:36 -08:00
parent abef2a7f45
commit 41edd61f3d
5 changed files with 88 additions and 81 deletions

View File

@@ -42,33 +42,15 @@ else:
from .psa import reader as psa_reader from .psa import reader as psa_reader
from .psa import importer as psa_importer from .psa import importer as psa_importer
import bpy import bpy
from bpy.props import PointerProperty from bpy.props import PointerProperty
classes = psx_types.__classes__ + \
# TODO: have the individual files emit a __classes__ field or something we can update it locally instead of explicitly declaring it here. psk_importer.__classes__ + \
classes = [] psk_exporter.__classes__ + \
classes.extend(psx_types.__classes__) psa_exporter.__classes__ + \
classes.extend(psk_exporter.__classes__) psa_importer.__classes__
classes.extend([
psk_importer.PskImportOperator,
psa_importer.PsaImportOperator,
psa_importer.PsaImportFileSelectOperator,
psa_exporter.PSA_UL_ExportActionList,
# psa_exporter.PSA_UL_ExportBoneGroupList,
psa_importer.PSA_UL_ImportActionList,
psa_importer.PsaImportActionListItem,
psa_importer.PsaImportPsaBoneItem,
psa_importer.PsaImportSelectAll,
psa_importer.PsaImportDeselectAll,
psa_importer.PSA_PT_ImportPanel,
psa_importer.PsaImportPropertyGroup,
psa_exporter.PsaExportOperator,
psa_exporter.PsaExportSelectAll,
psa_exporter.PsaExportDeselectAll,
psa_exporter.PsaExportActionListItem,
psa_exporter.PsaExportPropertyGroup,
])
def psk_export_menu_func(self, context): def psk_export_menu_func(self, context):
@@ -83,17 +65,12 @@ def psa_export_menu_func(self, context):
self.layout.operator(psa_exporter.PsaExportOperator.bl_idname, text='Unreal PSA (.psa)') self.layout.operator(psa_exporter.PsaExportOperator.bl_idname, text='Unreal PSA (.psa)')
def psa_import_menu_func(self, context):
self.layout.operator(psa_importer.PsaImportOperator.bl_idname, text='Unreal PSA (.psa)')
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) bpy.utils.register_class(cls)
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.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup) bpy.types.Scene.psa_export = PointerProperty(type=psa_exporter.PsaExportPropertyGroup)
bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup) bpy.types.Scene.psk_export = PointerProperty(type=psk_exporter.PskExportPropertyGroup)
@@ -105,7 +82,6 @@ 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

@@ -1,7 +1,7 @@
from typing import List from typing import List
def populate_bone_groups_list(armature_object, bone_group_list): def populate_bone_group_list(armature_object, bone_group_list):
bone_group_list.clear() bone_group_list.clear()
item = bone_group_list.add() item = bone_group_list.add()

View File

@@ -155,7 +155,7 @@ class PsaExportOperator(Operator, ExportHelper):
return {'CANCELLED'} return {'CANCELLED'}
# Populate bone groups list. # Populate bone groups list.
populate_bone_groups_list(self.armature, property_group.bone_group_list) populate_bone_group_list(self.armature, property_group.bone_group_list)
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
@@ -235,3 +235,13 @@ class PsaExportDeselectAll(bpy.types.Operator):
for action in property_group.action_list: for action in property_group.action_list:
action.is_selected = False action.is_selected = False
return {'FINISHED'} return {'FINISHED'}
__classes__ = [
PsaExportActionListItem,
PsaExportPropertyGroup,
PsaExportOperator,
PSA_UL_ExportActionList,
PsaExportSelectAll,
PsaExportDeselectAll,
]

View File

@@ -1,6 +1,5 @@
import bpy import bpy
import os import os
from math import inf
import numpy as np import numpy as np
from mathutils import Vector, Quaternion, Matrix from mathutils import Vector, Quaternion, Matrix
from .data import Psa from .data import Psa
@@ -9,17 +8,14 @@ from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature,
from bpy_extras.io_utils import ExportHelper, ImportHelper from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty
from .reader import PsaReader from .reader import PsaReader
import datetime
class PsaImporter(object): class PsaImporter(object):
def __init__(self): def __init__(self):
pass pass
def import_psa(self, psa_reader: PsaReader, sequence_names: List[AnyStr], context): def import_psa(self, psa_reader: PsaReader, sequence_names: List[AnyStr], armature_object):
property_group = context.scene.psa_import
sequences = map(lambda x: psa_reader.sequences[x], sequence_names) sequences = map(lambda x: psa_reader.sequences[x], sequence_names)
armature_object = property_group.armature_object
armature_data = armature_object.data armature_data = armature_object.data
class ImportBone(object): class ImportBone(object):
@@ -107,18 +103,8 @@ class PsaImporter(object):
import_bone.orig_quat = armature_bone.matrix_local.to_quaternion() import_bone.orig_quat = armature_bone.matrix_local.to_quaternion()
import_bone.post_quat = import_bone.orig_quat.conjugated() import_bone.post_quat = import_bone.orig_quat.conjugated()
io_time = datetime.timedelta()
math_time = datetime.timedelta()
keyframe_time = datetime.timedelta()
total_time = datetime.timedelta()
total_datetime_start = datetime.datetime.now()
# Create and populate the data for new sequences. # Create and populate the data for new sequences.
for sequence in sequences: for sequence in sequences:
# F-curve data buffer for all bones. This is used later on to avoid adding redundant keyframes.
next_frame_bones_fcurve_data = [(inf, inf, inf, inf, inf, inf, inf)] * len(import_bones)
# Add the action. # Add the action.
action = bpy.data.actions.new(name=sequence.name.decode()) action = bpy.data.actions.new(name=sequence.name.decode())
@@ -142,10 +128,8 @@ class PsaImporter(object):
sequence_name = sequence.name.decode('windows-1252') sequence_name = sequence.name.decode('windows-1252')
# Read the sequence data matrix from the PSA. # Read the sequence data matrix from the PSA.
start_datetime = datetime.datetime.now()
sequence_data_matrix = psa_reader.read_sequence_data_matrix(sequence_name) sequence_data_matrix = psa_reader.read_sequence_data_matrix(sequence_name)
keyframe_write_matrix = np.ones(sequence_data_matrix.shape, dtype=np.int8) keyframe_write_matrix = np.ones(sequence_data_matrix.shape, dtype=np.int8)
io_time += datetime.datetime.now() - start_datetime
# The first step is to determine the frames at which each bone will write out a keyframe. # The first step is to determine the frames at which each bone will write out a keyframe.
threshold = 0.001 threshold = 0.001
@@ -173,21 +157,10 @@ class PsaImporter(object):
# This bone has writeable keyframes for this frame. # This bone has writeable keyframes for this frame.
key_data = sequence_data_matrix[frame_index, bone_index] key_data = sequence_data_matrix[frame_index, bone_index]
# Calculate the local-space key data for the bone. # Calculate the local-space key data for the bone.
start_datetime = datetime.datetime.now()
fcurve_data = calculate_fcurve_data(import_bone, key_data) fcurve_data = calculate_fcurve_data(import_bone, key_data)
math_time += datetime.datetime.now() - start_datetime
for fcurve, should_write, datum in zip(import_bone.fcurves, keyframe_write_matrix[frame_index, bone_index], fcurve_data): for fcurve, should_write, datum in zip(import_bone.fcurves, keyframe_write_matrix[frame_index, bone_index], fcurve_data):
if should_write: if should_write:
start_datetime = datetime.datetime.now()
fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'}) fcurve.keyframe_points.insert(frame_index, datum, options={'FAST'})
keyframe_time += datetime.datetime.now() - start_datetime
total_time = datetime.datetime.now() - total_datetime_start
print(f'io_time: {io_time}')
print(f'math_time: {math_time}')
print(f'keyframe_time: {keyframe_time}')
print(f'total_time: {total_time}')
class PsaImportPsaBoneItem(PropertyGroup): class PsaImportPsaBoneItem(PropertyGroup):
@@ -209,6 +182,7 @@ class PsaImportActionListItem(PropertyGroup):
def on_psa_file_path_updated(property, context): def on_psa_file_path_updated(property, context):
print('PATH UPDATED')
property_group = context.scene.psa_import property_group = context.scene.psa_import
property_group.action_list.clear() property_group.action_list.clear()
property_group.psa_bones.clear() property_group.psa_bones.clear()
@@ -242,9 +216,9 @@ def on_armature_object_updated(property, context):
class PsaImportPropertyGroup(bpy.types.PropertyGroup): class PsaImportPropertyGroup(bpy.types.PropertyGroup):
psa_file_path: StringProperty(default='', subtype='FILE_PATH', update=on_psa_file_path_updated) psa_file_path: StringProperty(default='', update=on_psa_file_path_updated)
psa_bones: CollectionProperty(type=PsaImportPsaBoneItem) psa_bones: CollectionProperty(type=PsaImportPsaBoneItem)
armature_object: PointerProperty(name='Object', type=bpy.types.Object, update=on_armature_object_updated) # armature_object: PointerProperty(name='Object', type=bpy.types.Object, update=on_armature_object_updated)
action_list: CollectionProperty(type=PsaImportActionListItem) action_list: CollectionProperty(type=PsaImportActionListItem)
action_list_index: IntProperty(name='', default=0) action_list_index: IntProperty(name='', default=0)
action_filter_name: StringProperty(default='') action_filter_name: StringProperty(default='')
@@ -320,35 +294,62 @@ class PsaImportDeselectAll(bpy.types.Operator):
class PSA_PT_ImportPanel(Panel): class PSA_PT_ImportPanel(Panel):
bl_space_type = 'NLA_EDITOR' bl_space_type = 'PROPERTIES'
bl_region_type = 'UI' bl_region_type = 'WINDOW'
bl_label = 'PSA Import' bl_label = 'PSA Import'
bl_context = 'object' bl_context = 'data'
bl_category = 'PSA Import' bl_category = 'PSA Import'
bl_options = {'DEFAULT_CLOSED'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.view_layer.objects.active is not None return context.object.type == 'ARMATURE'
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
property_group = context.scene.psa_import property_group = context.scene.psa_import
row = layout.row() row = layout.row()
row.prop(property_group, 'psa_file_path', text='PSA File') row.prop(property_group, 'psa_file_path', text='')
row.enabled = False
# row.enabled = property_group.psa_file_path is not ''
row = layout.row() row = layout.row()
row.prop_search(property_group, 'armature_object', bpy.data, 'objects')
box = layout.box() layout.separator()
box.label(text=f'Actions ({len(property_group.action_list)})', icon='ACTION')
row = box.row() row.operator('psa_import.select_file', text='Select PSA File', icon='FILEBROWSER')
rows = max(3, min(len(property_group.action_list), 10)) if len(property_group.action_list) > 0:
row.template_list('PSA_UL_ImportActionList', '', property_group, 'action_list', property_group, 'action_list_index', rows=rows) box = layout.box()
row = box.row(align=True) box.label(text=f'Actions ({len(property_group.action_list)})', icon='ACTION')
row.label(text='Select') row = box.row()
row.operator('psa_import.actions_select_all', text='All') rows = max(3, min(len(property_group.action_list), 10))
row.operator('psa_import.actions_deselect_all', text='None') row.template_list('PSA_UL_ImportActionList', '', property_group, 'action_list', property_group, 'action_list_index', rows=rows)
row = box.row(align=True)
row.label(text='Select')
row.operator('psa_import.actions_select_all', text='All')
row.operator('psa_import.actions_deselect_all', text='None')
layout.separator()
layout.operator('psa_import.import', text=f'Import') layout.operator('psa_import.import', text=f'Import')
class PsaImportSelectFile(Operator):
bl_idname = "psa_import.select_file"
bl_label = "Select"
bl_options = {'REGISTER', 'UNDO'}
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
filter_glob: bpy.props.StringProperty(default="*.psa", options={"HIDDEN"})
def execute(self, context):
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): class PsaImportOperator(Operator):
bl_idname = 'psa_import.import' bl_idname = 'psa_import.import'
bl_label = 'Import' bl_label = 'Import'
@@ -356,16 +357,17 @@ class PsaImportOperator(Operator):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
property_group = context.scene.psa_import property_group = context.scene.psa_import
active_object = context.view_layer.objects.active
action_list = property_group.action_list action_list = property_group.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list)) has_selected_actions = any(map(lambda action: action.is_selected, action_list))
armature_object = property_group.armature_object return has_selected_actions and active_object is not None and active_object.type == 'ARMATURE'
return has_selected_actions and armature_object is not None
def execute(self, context): def execute(self, context):
property_group = context.scene.psa_import property_group = context.scene.psa_import
psa_reader = PsaReader(property_group.psa_file_path) psa_reader = PsaReader(property_group.psa_file_path)
sequence_names = [x.action_name for x in property_group.action_list if x.is_selected] sequence_names = [x.action_name for x in property_group.action_list if x.is_selected]
PsaImporter().import_psa(psa_reader, sequence_names, context) PsaImporter().import_psa(psa_reader, sequence_names, context.view_layer.objects.active)
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
return {'FINISHED'} return {'FINISHED'}
@@ -389,3 +391,17 @@ class PsaImportFileSelectOperator(Operator, ImportHelper):
property_group.psa_file_path = self.filepath property_group.psa_file_path = self.filepath
# Load the sequence names from the selected file # Load the sequence names from the selected file
return {'FINISHED'} return {'FINISHED'}
__classes__ = [
PsaImportPsaBoneItem,
PsaImportActionListItem,
PsaImportPropertyGroup,
PSA_UL_ImportActionList,
PsaImportSelectAll,
PsaImportDeselectAll,
PSA_PT_ImportPanel,
PsaImportOperator,
PsaImportFileSelectOperator,
PsaImportSelectFile,
]

View File

@@ -182,3 +182,8 @@ class PskImportOperator(Operator, ImportHelper):
name = os.path.splitext(os.path.basename(self.filepath))[0] name = os.path.splitext(os.path.basename(self.filepath))[0]
PskImporter().import_psk(psk, name, context) PskImporter().import_psk(psk, name, context)
return {'FINISHED'} return {'FINISHED'}
__classes__ = [
PskImportOperator
]