1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2024-11-15 02:37:39 +01:00

PSA Import screen now has more robust functionality now (but still aint done!)

This commit is contained in:
Colin Basnett 2022-01-24 21:50:34 -08:00
parent 0d06236bab
commit c672941663
8 changed files with 267 additions and 87 deletions

View File

@ -13,6 +13,7 @@ bl_info = {
if 'bpy' in locals():
import importlib
importlib.reload(psx_data)
importlib.reload(psx_helpers)
importlib.reload(psx_types)
@ -42,15 +43,14 @@ else:
from .psa import reader as psa_reader
from .psa import importer as psa_importer
import bpy
from bpy.props import PointerProperty
classes = psx_types.__classes__ + \
psk_importer.__classes__ + \
psk_exporter.__classes__ + \
psa_exporter.__classes__ + \
psa_importer.__classes__
classes = (psx_types.classes +
psk_importer.classes +
psk_exporter.classes +
psa_exporter.classes +
psa_importer.classes)
def psk_export_menu_func(self, context):

View File

@ -17,8 +17,11 @@ def populate_bone_group_list(armature_object, bone_group_list):
item.is_selected = True
def add_bone_groups_to_layout(layout):
pass
def get_psa_sequence_name(action, should_use_original_sequence_name):
if should_use_original_sequence_name and 'original_sequence_name' in action:
return action['original_sequence_name']
else:
return action.name
def get_export_bone_indices_for_bone_groups(armature_object, bone_group_indices: List[int]) -> List[int]:

View File

@ -108,10 +108,7 @@ class PsaBuilder(object):
sequence = Psa.Sequence()
if options.should_use_original_sequence_names and 'original_sequence_name' in action:
sequence_name = action['original_sequence_name']
else:
sequence_name = action.name
sequence_name = get_psa_sequence_name(action, options.should_use_original_sequence_names)
sequence.name = bytes(sequence_name, encoding='windows-1252')
sequence.frame_count = frame_max - frame_min + 1

View File

@ -1,5 +1,5 @@
import bpy
from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup
from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup, Panel
from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty, EnumProperty
from bpy_extras.io_utils import ExportHelper
from typing import Type
@ -7,6 +7,7 @@ from .builder import PsaBuilder, PsaBuilderOptions
from .data import *
from ..types import BoneGroupListItem
from ..helpers import *
from collections import Counter
import re
@ -49,15 +50,13 @@ def update_action_names(context):
property_group = context.scene.psa_export
for item in property_group.action_list:
action = item.action
if property_group.should_use_original_sequence_names and 'original_sequence_name' in action:
item.action_name = action['original_sequence_name']
else:
item.action_name = action.name
item.action_name = get_psa_sequence_name(action, property_group.should_use_original_sequence_names)
def should_use_original_sequence_names_updated(property, context):
update_action_names(context)
class PsaExportPropertyGroup(PropertyGroup):
action_list: CollectionProperty(type=PsaExportActionListItem)
action_list_index: IntProperty(default=0)
@ -71,11 +70,11 @@ class PsaExportPropertyGroup(PropertyGroup):
)
bone_group_list: CollectionProperty(type=BoneGroupListItem)
bone_group_list_index: IntProperty(default=0)
should_use_original_sequence_names: BoolProperty(default=False, description='If the action was imported from the PSA Import panel, the original name of the action will be used instead of the action name assigned in Blender', update=should_use_original_sequence_names_updated)
should_use_original_sequence_names: BoolProperty(default=False, name='Original Names', description='If the action was imported from the PSA Import panel, the original name of the sequence will be used instead of the Blender action name', update=should_use_original_sequence_names_updated)
def is_bone_filter_mode_item_available(context, identifier):
if identifier == "BONE_GROUPS":
if identifier == 'BONE_GROUPS':
obj = context.active_object
if not obj.pose or not obj.pose.bone_groups:
return False
@ -83,7 +82,7 @@ def is_bone_filter_mode_item_available(context, identifier):
class PsaExportOperator(Operator, ExportHelper):
bl_idname = 'export.psa'
bl_idname = 'psa_export.operator'
bl_label = 'Export'
__doc__ = 'Export actions to PSA'
filename_ext = '.psa'
@ -102,19 +101,34 @@ class PsaExportOperator(Operator, ExportHelper):
property_group = context.scene.psa_export
# ACTIONS
box = layout.box()
box.label(text='Actions', icon='ACTION')
row = box.row()
row.template_list('PSA_UL_ExportActionList', 'asd', property_group, 'action_list', property_group, 'action_list_index', rows=10)
row = box.row(align=True)
layout.label(text='Actions', icon='ACTION')
row = layout.row(align=True)
row.label(text='Select')
row.operator('psa_export.actions_select_all', text='All')
row.operator('psa_export.actions_deselect_all', text='None')
row = layout.row()
rows = max(3, min(len(property_group.action_list), 10))
row.template_list('PSA_UL_ExportActionList', '', property_group, 'action_list', property_group,
'action_list_index', rows=rows)
layout.prop(property_group, 'should_use_original_sequence_names', text='Original Sequence Names')
col = layout.column(heading="Options")
col.use_property_split = True
col.use_property_decorate = False
col.prop(property_group, 'should_use_original_sequence_names')
# Determine if there is going to be a naming conflict and display an error, if so.
selected_actions = [x for x in property_group.action_list if x.is_selected]
action_names = [x.action_name for x in selected_actions]
action_name_counts = Counter(action_names)
for action_name, count in action_name_counts.items():
if count > 1:
layout.label(text=f'Duplicate action: {action_name}', icon='ERROR')
break
layout.separator()
# BONES
box = layout.box()
box = layout.row()
box.label(text='Bones', icon='BONE_DATA')
bone_filter_mode_items = property_group.bl_rna.properties['bone_filter_mode'].enum_items_static
row = box.row(align=True)
@ -126,10 +140,9 @@ class PsaExportOperator(Operator, ExportHelper):
item_layout.enabled = is_bone_filter_mode_item_available(context, identifier)
if property_group.bone_filter_mode == 'BONE_GROUPS':
box = layout.box()
row = box.row()
rows = max(3, min(len(property_group.bone_group_list), 10))
row.template_list('PSX_UL_BoneGroupList', '', property_group, 'bone_group_list', property_group, 'bone_group_list_index', rows=rows)
layout.template_list('PSX_UL_BoneGroupList', '', property_group, 'bone_group_list', property_group,
'bone_group_list_index', rows=rows)
def is_action_for_armature(self, action):
if len(action.fcurves) == 0:
@ -160,12 +173,12 @@ class PsaExportOperator(Operator, ExportHelper):
# Populate actions list.
property_group.action_list.clear()
for action in bpy.data.actions:
if not self.is_action_for_armature(action):
continue
item = property_group.action_list.add()
item.action = action
item.action_name = action.name
if self.is_action_for_armature(action):
item.is_selected = True
item.is_selected = True
update_action_names(context)
@ -264,11 +277,11 @@ class PsaExportDeselectAll(bpy.types.Operator):
return {'FINISHED'}
__classes__ = [
classes = (
PsaExportActionListItem,
PsaExportPropertyGroup,
PsaExportOperator,
PSA_UL_ExportActionList,
PsaExportSelectAll,
PsaExportDeselectAll,
]
)

View File

@ -5,7 +5,7 @@ from mathutils import Vector, Quaternion, Matrix
from .data import Psa
from typing import List, AnyStr, Optional
from bpy.types import Operator, Action, UIList, PropertyGroup, Panel, Armature, FileSelectParams
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy_extras.io_utils import ImportHelper
from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty
from .reader import PsaReader
@ -16,6 +16,7 @@ class PsaImportOptions(object):
self.should_use_fake_user = False
self.should_stash = False
self.sequence_names = []
self.should_use_action_name_prefix = False
self.action_name_prefix = ''
@ -216,40 +217,57 @@ class PsaImportActionListItem(PropertyGroup):
return self.action_name
def on_psa_file_path_updated(property, context):
def load_psa_file(context):
property_group = context.scene.psa_import
property_group.action_list.clear()
property_group.sequence_list.clear()
property_group.psa_bones.clear()
property_group.action_list.clear()
property_group.psa_error = ''
try:
# Read the file and populate the action list.
p = os.path.abspath(property_group.psa_file_path)
psa_reader = PsaReader(p)
for sequence in psa_reader.sequences.values():
item = property_group.action_list.add()
item = property_group.sequence_list.add()
item.action_name = sequence.name.decode('windows-1252')
item.frame_count = sequence.frame_count
item.is_selected = True
for psa_bone in psa_reader.bones:
item = property_group.psa_bones.add()
item.bone_name = psa_bone.name
except IOError as e:
# TODO: set an error somewhere so the user knows the PSA could not be read.
pass
item.bone_name = psa_bone.name.decode('windows-1252')
except Exception as e:
property_group.psa_error = str(e)
class PsaImportPropertyGroup(bpy.types.PropertyGroup):
def on_psa_file_path_updated(property, context):
load_psa_file(context)
class PsaBonePropertyGroup(PropertyGroup):
bone_name: StringProperty()
class PsaDataPropertyGroup(PropertyGroup):
bone_count: IntProperty(default=0)
bones: CollectionProperty(type=PsaBonePropertyGroup)
sequence_count: IntProperty(default=0)
class PsaImportPropertyGroup(PropertyGroup):
psa_file_path: StringProperty(default='', update=on_psa_file_path_updated, name='PSA File Path')
psa_error: StringProperty(default='')
psa_bones: CollectionProperty(type=PsaImportPsaBoneItem)
sequence_list: CollectionProperty(type=PsaImportActionListItem)
sequence_list_index: IntProperty(name='', default=0)
action_list: CollectionProperty(type=PsaImportActionListItem)
action_list_index: IntProperty(name='', default=0)
action_filter_name: StringProperty(default='')
should_clean_keys: BoolProperty(default=True, name='Clean Keyframes', description='Exclude unnecessary keyframes from being written to the actions.')
should_use_fake_user: BoolProperty(default=True, name='Fake User', description='Assign each imported action a fake user so that the data block is saved even it has no users.')
should_stash: BoolProperty(default=False, name='Stash', description='Stash each imported action as a strip on a new non-contributing NLA track')
action_name_prefix: StringProperty(default='', name='Action Name Prefix')
should_use_action_name_prefix: BoolProperty(default=False, name='Prefix Action Name')
action_name_prefix: StringProperty(default='', name='Prefix')
class PSA_UL_ImportActionList(UIList):
class PSA_UL_SequenceList(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
row = layout.row(align=True)
@ -282,7 +300,34 @@ class PSA_UL_ImportActionList(UIList):
return flt_flags, flt_neworder
class PsaImportSelectAll(bpy.types.Operator):
class PSA_UL_ImportSequenceList(PSA_UL_SequenceList, UIList):
pass
class PSA_UL_ImportActionList(PSA_UL_SequenceList, UIList):
pass
class PsaImportSequencesSelectAll(bpy.types.Operator):
bl_idname = 'psa_import.sequences_select_all'
bl_label = 'All'
bl_description = 'Select all sequences'
@classmethod
def poll(cls, context):
property_group = context.scene.psa_import
sequence_list = property_group.sequence_list
has_unselected_actions = any(map(lambda action: not action.is_selected, sequence_list))
return len(sequence_list) > 0 and has_unselected_actions
def execute(self, context):
property_group = context.scene.psa_import
for action in property_group.sequence_list:
action.is_selected = True
return {'FINISHED'}
class PsaImportActionsSelectAll(bpy.types.Operator):
bl_idname = 'psa_import.actions_select_all'
bl_label = 'All'
bl_description = 'Select all actions'
@ -301,7 +346,26 @@ class PsaImportSelectAll(bpy.types.Operator):
return {'FINISHED'}
class PsaImportDeselectAll(bpy.types.Operator):
class PsaImportSequencesDeselectAll(bpy.types.Operator):
bl_idname = 'psa_import.sequences_deselect_all'
bl_label = 'None'
bl_description = 'Deselect all sequences'
@classmethod
def poll(cls, context):
property_group = context.scene.psa_import
sequence_list = property_group.sequence_list
has_selected_sequences = any(map(lambda action: action.is_selected, sequence_list))
return len(sequence_list) > 0 and has_selected_sequences
def execute(self, context):
property_group = context.scene.psa_import
for action in property_group.sequence_list:
action.is_selected = False
return {'FINISHED'}
class PsaImportActionsDeselectAll(bpy.types.Operator):
bl_idname = 'psa_import.actions_deselect_all'
bl_label = 'None'
bl_description = 'Deselect all actions'
@ -320,6 +384,31 @@ class PsaImportDeselectAll(bpy.types.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'
def draw(self, context):
layout = self.layout
property_group = context.scene.psa_import
col = layout.column(heading="Options")
col.use_property_split = True
col.use_property_decorate = False
col.prop(property_group, 'should_clean_keys')
col.separator()
col.prop(property_group, 'should_use_fake_user')
col.prop(property_group, 'should_stash')
col.separator()
col.prop(property_group, 'should_use_action_name_prefix')
if property_group.should_use_action_name_prefix:
col.prop(property_group, 'action_name_prefix')
class PSA_PT_ImportPanel(Panel):
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
@ -336,33 +425,58 @@ class PSA_PT_ImportPanel(Panel):
layout = self.layout
property_group = context.scene.psa_import
row = layout.row()
row = layout.row(align=True)
row.operator('psa_import.select_file', text='', icon='FILEBROWSER')
row.prop(property_group, 'psa_file_path', text='')
row.enabled = False
row.operator('psa_import.file_reload', text='', icon='FILE_REFRESH')
row = layout.row()
row.operator('psa_import.select_file', text='Select PSA File', icon='FILEBROWSER')
if property_group.psa_error != '':
row = layout.row()
row.label(text='File could not be read', icon='ERROR')
if len(property_group.action_list) > 0:
box = layout.box()
box.label(text=f'Actions ({len(property_group.action_list)})', icon='ACTION')
row = box.row()
rows = max(3, min(len(property_group.action_list), 10))
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')
box = layout.box()
col = layout.column(heading="Options")
col.use_property_split = True
col.use_property_decorate = False
col.prop(property_group, 'should_clean_keys')
col.prop(property_group, 'should_use_fake_user')
col.prop(property_group, 'should_stash')
col.prop(property_group, 'action_name_prefix')
box.label(text=f'Sequences', icon='ARMATURE_DATA')
layout.operator('psa_import.import', text=f'Import')
# select
rows = max(3, min(len(property_group.sequence_list) + len(property_group.action_list), 10))
row = box.row()
col = row.column()
row2 = col.row(align=True)
row2.label(text='Select')
row2.operator('psa_import.sequences_select_all', text='All')
row2.operator('psa_import.sequences_deselect_all', text='None')
col = col.row()
col.template_list('PSA_UL_ImportSequenceList', '', property_group, 'sequence_list', property_group,
'sequence_list_index', rows=rows)
col = row.column(align=True)
col.operator('psa_import.push_to_actions', icon='TRIA_RIGHT', text='')
col.operator('psa_import.pop_from_actions', icon='TRIA_LEFT', text='')
col = row.column()
row2 = col.row(align=True)
row2.label(text='Select')
row2.operator('psa_import.actions_select_all', text='All')
row2.operator('psa_import.actions_deselect_all', text='None')
col.template_list('PSA_UL_ImportActionList', '', property_group, 'action_list', property_group,
'action_list_index', rows=rows)
col.separator()
col.operator('psa_import.import', text=f'Import')
class PsaImportFileReload(Operator):
bl_idname = 'psa_import.file_reload'
bl_label = 'Refresh'
bl_options = {'REGISTER'}
bl_description = 'Refresh the PSA file'
def execute(self, context):
load_psa_file(context)
return {"FINISHED"}
class PsaImportSelectFile(Operator):
@ -392,13 +506,12 @@ class PsaImportOperator(Operator):
property_group = context.scene.psa_import
active_object = context.view_layer.objects.active
action_list = property_group.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list))
return has_selected_actions and active_object is not None and active_object.type == 'ARMATURE'
return len(action_list) and active_object is not None and active_object.type == 'ARMATURE'
def execute(self, context):
property_group = context.scene.psa_import
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]
options = PsaImportOptions()
options.sequence_names = sequence_names
options.should_clean_keys = property_group.should_clean_keys
@ -410,6 +523,52 @@ class PsaImportOperator(Operator):
return {'FINISHED'}
class PsaImportPushToActions(Operator):
bl_idname = 'psa_import.push_to_actions'
bl_label = 'Push to Actions'
@classmethod
def poll(cls, context):
property_group = context.scene.psa_import
has_sequences_selected = any(map(lambda x: x.is_selected, property_group.sequence_list))
return has_sequences_selected
def execute(self, context):
property_group = context.scene.psa_import
indices_to_remove = []
for sequence_index, item in enumerate(property_group.sequence_list):
if item.is_selected:
indices_to_remove.append(sequence_index)
action = property_group.action_list.add()
action.action_name = item.action_name
for index in reversed(indices_to_remove):
property_group.sequence_list.remove(index)
return {'FINISHED'}
class PsaImportPopFromActions(Operator):
bl_idname = 'psa_import.pop_from_actions'
bl_label = 'Pop From Actions'
@classmethod
def poll(cls, context):
property_group = context.scene.psa_import
has_actions_selected = any(map(lambda x: x.is_selected, property_group.action_list))
return has_actions_selected
def execute(self, context):
property_group = context.scene.psa_import
indices_to_remove = []
for action_index, item in enumerate(property_group.action_list):
if item.is_selected:
indices_to_remove.append(action_index)
sequence = property_group.sequence_list.add()
sequence.action_name = item.action_name
for index in reversed(indices_to_remove):
property_group.action_list.remove(index)
return {'FINISHED'}
class PsaImportFileSelectOperator(Operator, ImportHelper):
bl_idname = 'psa_import.file_select'
bl_label = 'File Select'
@ -432,15 +591,23 @@ class PsaImportFileSelectOperator(Operator, ImportHelper):
return {'FINISHED'}
__classes__ = [
classes = (
PsaImportPsaBoneItem,
PsaImportActionListItem,
PsaImportPropertyGroup,
PSA_UL_SequenceList,
PSA_UL_ImportSequenceList,
PSA_UL_ImportActionList,
PsaImportSelectAll,
PsaImportDeselectAll,
PsaImportSequencesSelectAll,
PsaImportSequencesDeselectAll,
PsaImportActionsSelectAll,
PsaImportActionsDeselectAll,
PsaImportFileReload,
PSA_PT_ImportPanel,
PSA_PT_ImportPanel_Advanced,
PsaImportOperator,
PsaImportFileSelectOperator,
PsaImportSelectFile,
]
PsaImportPushToActions,
PsaImportPopFromActions,
)

View File

@ -148,7 +148,7 @@ class PskExportPropertyGroup(PropertyGroup):
bone_group_list_index: IntProperty(default=0)
__classes__ = [
classes = (
PskExportOperator,
PskExportPropertyGroup
]
)

View File

@ -184,6 +184,6 @@ class PskImportOperator(Operator, ImportHelper):
return {'FINISHED'}
__classes__ = [
PskImportOperator
]
classes = (
PskImportOperator,
)

View File

@ -19,7 +19,7 @@ class BoneGroupListItem(PropertyGroup):
return self.name
__classes__ = [
classes = (
BoneGroupListItem,
PSX_UL_BoneGroupList
]
PSX_UL_BoneGroupList,
)