diff --git a/io_scene_psk_psa/helpers.py b/io_scene_psk_psa/helpers.py index 4cdc737..54c1b2c 100644 --- a/io_scene_psk_psa/helpers.py +++ b/io_scene_psk_psa/helpers.py @@ -1,10 +1,10 @@ import datetime -from collections import Counter import re +from collections import Counter from typing import List, Iterable +import bpy.types from bpy.types import NlaStrip, Object -from .types import BoneGroupListItem class Timer: @@ -47,7 +47,7 @@ def get_nla_strips_in_timeframe(animation_data, frame_min, frame_max) -> List[Nl return strips -def populate_bone_group_list(armature_object: Object, bone_group_list: Iterable[BoneGroupListItem]) -> None: +def populate_bone_group_list(armature_object: Object, bone_group_list: bpy.types.Collection) -> None: """ Updates the bone group collection. diff --git a/io_scene_psk_psa/psa/builder.py b/io_scene_psk_psa/psa/builder.py index 969b8c6..f57361d 100644 --- a/io_scene_psk_psa/psa/builder.py +++ b/io_scene_psk_psa/psa/builder.py @@ -1,4 +1,4 @@ -from typing import Dict, Iterable +from typing import Dict from bpy.types import Action @@ -217,7 +217,8 @@ def build_psa(context, options: PsaBuildOptions) -> Psa: # Add prefixes and suffices to the names of the export sequences and strip whitespace. for export_sequence in export_sequences: - export_sequence.name = f'{options.sequence_name_prefix}{export_sequence.name}{options.sequence_name_suffix}'.strip() + export_sequence.name = f'{options.sequence_name_prefix}{export_sequence.name}{options.sequence_name_suffix}' + export_sequence.name = export_sequence.name.strip() # Now build the PSA sequences. # We actually alter the timeline frame and simply record the resultant pose bone matrices. diff --git a/io_scene_psk_psa/psa/exporter.py b/io_scene_psk_psa/psa/exporter.py index 90e93ba..59bd82d 100644 --- a/io_scene_psk_psa/psa/exporter.py +++ b/io_scene_psk_psa/psa/exporter.py @@ -1,7 +1,5 @@ import fnmatch -import re import sys -from collections import Counter from typing import Type import bpy @@ -16,17 +14,19 @@ from ..helpers import * from ..types import BoneGroupListItem +def write_section(fp, name: bytes, data_type: Type[Structure] = None, data: list = None): + section = Section() + section.name = name + if data_type is not None and data is not None: + section.data_size = sizeof(data_type) + section.data_count = len(data) + fp.write(section) + if data is not None: + for datum in data: + fp.write(datum) + + def export_psa(psa: Psa, path: str): - def write_section(fp, name: bytes, data_type: Type[Structure] = None, data: list = None): - section = Section() - section.name = name - if data_type is not None and data is not None: - section.data_size = sizeof(data_type) - section.data_count = len(data) - fp.write(section) - if data is not None: - for datum in data: - fp.write(datum) with open(path, 'wb') as fp: write_section(fp, b'ANIMHEAD') write_section(fp, b'BONENAMES', Psa.Bone, psa.bones) @@ -61,16 +61,19 @@ def psa_export_property_group_animation_data_override_poll(_context, obj): return obj.animation_data is not None +empty_set = set() + + class PsaExportPropertyGroup(PropertyGroup): root_motion: BoolProperty( name='Root Motion', - options=set(), + options=empty_set, default=False, description='The root bone will be transformed as it appears in the scene', ) should_override_animation_data: BoolProperty( name='Override Animation Data', - options=set(), + options=empty_set, default=False, description='Use the animation data from a different object instead of the selected object' ) @@ -80,7 +83,7 @@ class PsaExportPropertyGroup(PropertyGroup): ) sequence_source: EnumProperty( name='Source', - options=set(), + options=empty_set, description='', items=( ('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0), @@ -90,7 +93,7 @@ class PsaExportPropertyGroup(PropertyGroup): ) fps_source: EnumProperty( name='FPS Source', - options=set(), + options=empty_set, description='', items=( ('SCENE', 'Scene', '', 'SCENE_DATA', 0), @@ -100,7 +103,7 @@ class PsaExportPropertyGroup(PropertyGroup): ('CUSTOM', 'Custom', '', 2) ) ) - fps_custom: FloatProperty(default=30.0, min=sys.float_info.epsilon, soft_min=1.0, options=set(), step=100, + fps_custom: FloatProperty(default=30.0, min=sys.float_info.epsilon, soft_min=1.0, options=empty_set, step=100, soft_max=60.0) action_list: CollectionProperty(type=PsaExportActionListItem) action_list_index: IntProperty(default=0) @@ -108,7 +111,7 @@ class PsaExportPropertyGroup(PropertyGroup): marker_list_index: IntProperty(default=0) bone_filter_mode: EnumProperty( name='Bone Filter', - options=set(), + options=empty_set, description='', items=( ('ALL', 'All', 'All bones will be exported.'), @@ -121,7 +124,7 @@ class PsaExportPropertyGroup(PropertyGroup): should_use_original_sequence_names: BoolProperty( default=False, name='Original Names', - options=set(), + options=empty_set, update=should_use_original_sequence_names_updated, 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', @@ -129,12 +132,12 @@ class PsaExportPropertyGroup(PropertyGroup): should_trim_timeline_marker_sequences: BoolProperty( default=True, name='Trim Sequences', - options=set(), + options=empty_set, description='Frames without NLA track information at the boundaries of timeline markers will be excluded from ' 'the exported sequences ' ) - sequence_name_prefix: StringProperty(name='Prefix', options=set()) - sequence_name_suffix: StringProperty(name='Suffix', options=set()) + sequence_name_prefix: StringProperty(name='Prefix', options=empty_set) + sequence_name_suffix: StringProperty(name='Suffix', options=empty_set) sequence_filter_name: StringProperty( default='', name='Filter by Name', @@ -143,14 +146,14 @@ class PsaExportPropertyGroup(PropertyGroup): sequence_use_filter_invert: BoolProperty( default=False, name='Invert', - options=set(), + options=empty_set, description='Invert filtering (show hidden items, and vice versa)') sequence_filter_asset: BoolProperty( default=False, name='Show assets', - options=set(), + options=empty_set, description='Show actions that belong to an asset library') - sequence_use_filter_sort_reverse: BoolProperty(default=True, options=set()) + sequence_use_filter_sort_reverse: BoolProperty(default=True, options=empty_set) def is_bone_filter_mode_item_available(context, identifier): @@ -182,13 +185,13 @@ class PsaExportOperator(Operator, ExportHelper): try: cls._check_context(context) except RuntimeError as e: - cls.poll_message_set((str(e))) + cls.poll_message_set(str(e)) return False return True def draw(self, context): layout = self.layout - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') # FPS layout.prop(pg, 'fps_source', text='FPS') @@ -291,7 +294,7 @@ class PsaExportOperator(Operator, ExportHelper): except RuntimeError as e: self.report({'ERROR_INVALID_CONTEXT'}, str(e)) - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') self.armature = context.view_layer.objects.active # Populate actions list. @@ -326,7 +329,7 @@ class PsaExportOperator(Operator, ExportHelper): return {'RUNNING_MODAL'} def execute(self, context): - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') actions = [x.action for x in pg.action_list if x.is_selected] marker_names = [x.name for x in pg.marker_list if x.is_selected] @@ -349,6 +352,7 @@ class PsaExportOperator(Operator, ExportHelper): try: psa = build_psa(context, options) + self.report({'INFO'}, f'PSA export successful') except RuntimeError as e: self.report({'ERROR_INVALID_CONTEXT'}, str(e)) return {'CANCELLED'} @@ -358,7 +362,7 @@ class PsaExportOperator(Operator, ExportHelper): return {'FINISHED'} -def filter_sequences(pg: PsaExportPropertyGroup, sequences: bpy.types.bpy_prop_collection) -> List[int]: +def filter_sequences(pg: PsaExportPropertyGroup, sequences) -> List[int]: bitflag_filter_item = 1 << 30 flt_flags = [bitflag_filter_item] * len(sequences) @@ -381,7 +385,7 @@ def filter_sequences(pg: PsaExportPropertyGroup, sequences: bpy.types.bpy_prop_c return flt_flags -def get_visible_sequences(pg: PsaExportPropertyGroup, sequences: bpy.types.bpy_prop_collection) -> List[PsaExportActionListItem]: +def get_visible_sequences(pg: PsaExportPropertyGroup, sequences) -> List[PsaExportActionListItem]: visible_sequences = [] for i, flag in enumerate(filter_sequences(pg, sequences)): if bool(flag & (1 << 30)): @@ -402,7 +406,7 @@ class PSA_UL_ExportSequenceList(UIList): layout.label(text='', icon='ASSET_MANAGER') def draw_filter(self, context, layout): - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') row = layout.row() subrow = row.row(align=True) subrow.prop(pg, 'sequence_filter_name', text="") @@ -414,7 +418,7 @@ class PSA_UL_ExportSequenceList(UIList): subrow.prop(pg, 'sequence_filter_asset', icon_only=True, icon='ASSET_MANAGER') def filter_items(self, context, data, prop): - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') actions = getattr(data, prop) flt_flags = filter_sequences(pg, actions) flt_neworder = bpy.types.UI_UL_list.sort_items_by_name(actions, 'name') @@ -438,14 +442,14 @@ class PsaExportActionsSelectAll(Operator): @classmethod def poll(cls, context): - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') item_list = cls.get_item_list(context) visible_sequences = get_visible_sequences(pg, item_list) has_unselected_sequences = any(map(lambda item: not item.is_selected, visible_sequences)) return has_unselected_sequences def execute(self, context): - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') sequences = self.get_item_list(context) for sequence in get_visible_sequences(pg, sequences): sequence.is_selected = True @@ -474,7 +478,7 @@ class PsaExportActionsDeselectAll(Operator): return len(item_list) > 0 and has_selected_items def execute(self, context): - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') item_list = self.get_item_list(context) for sequence in get_visible_sequences(pg, item_list): sequence.is_selected = False @@ -489,13 +493,13 @@ class PsaExportBoneGroupsSelectAll(Operator): @classmethod def poll(cls, context): - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') item_list = pg.bone_group_list has_unselected_items = any(map(lambda action: not action.is_selected, item_list)) return len(item_list) > 0 and has_unselected_items def execute(self, context): - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') for item in pg.bone_group_list: item.is_selected = True return {'FINISHED'} @@ -509,13 +513,13 @@ class PsaExportBoneGroupsDeselectAll(Operator): @classmethod def poll(cls, context): - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') item_list = pg.bone_group_list has_selected_actions = any(map(lambda action: action.is_selected, item_list)) return len(item_list) > 0 and has_selected_actions def execute(self, context): - pg = context.scene.psa_export + pg = getattr(context.scene, 'psa_export') for action in pg.bone_group_list: action.is_selected = False return {'FINISHED'} diff --git a/io_scene_psk_psa/psa/importer.py b/io_scene_psk_psa/psa/importer.py index 0b60958..7150230 100644 --- a/io_scene_psk_psa/psa/importer.py +++ b/io_scene_psk_psa/psa/importer.py @@ -26,38 +26,40 @@ class PsaImportOptions(object): self.action_name_prefix = '' +class ImportBone(object): + def __init__(self, psa_bone: Psa.Bone): + self.psa_bone: Psa.Bone = psa_bone + self.parent: Optional[ImportBone] = None + self.armature_bone = None + self.pose_bone = None + self.orig_loc: Vector = Vector() + self.orig_quat: Quaternion = Quaternion() + self.post_quat: Quaternion = Quaternion() + self.fcurves = [] + + +def calculate_fcurve_data(import_bone: ImportBone, key_data: []): + # Convert world-space transforms to local-space transforms. + key_rotation = Quaternion(key_data[0:4]) + key_location = Vector(key_data[4:]) + q = import_bone.post_quat.copy() + q.rotate(import_bone.orig_quat) + quat = q + q = import_bone.post_quat.copy() + if import_bone.parent is None: + q.rotate(key_rotation.conjugated()) + else: + q.rotate(key_rotation) + quat.rotate(q.conjugated()) + loc = key_location - import_bone.orig_loc + loc.rotate(import_bone.post_quat.conjugated()) + return quat.w, quat.x, quat.y, quat.z, loc.x, loc.y, loc.z + + def import_psa(psa_reader: PsaReader, armature_object, options: PsaImportOptions): sequences = map(lambda x: psa_reader.sequences[x], options.sequence_names) armature_data = armature_object.data - class ImportBone(object): - def __init__(self, psa_bone: Psa.Bone): - self.psa_bone: Psa.Bone = psa_bone - self.parent: Optional[ImportBone] = None - self.armature_bone = None - self.pose_bone = None - self.orig_loc: Vector = Vector() - self.orig_quat: Quaternion = Quaternion() - self.post_quat: Quaternion = Quaternion() - self.fcurves = [] - - def calculate_fcurve_data(import_bone: ImportBone, key_data: []): - # Convert world-space transforms to local-space transforms. - key_rotation = Quaternion(key_data[0:4]) - key_location = Vector(key_data[4:]) - q = import_bone.post_quat.copy() - q.rotate(import_bone.orig_quat) - quat = q - q = import_bone.post_quat.copy() - if import_bone.parent is None: - q.rotate(key_rotation.conjugated()) - else: - q.rotate(key_rotation) - quat.rotate(q.conjugated()) - loc = key_location - import_bone.orig_loc - loc.rotate(import_bone.post_quat.conjugated()) - return quat.w, quat.x, quat.y, quat.z, loc.x, loc.y, loc.z - # Create an index mapping from bones in the PSA to bones in the target armature. psa_to_armature_bone_indices = {} armature_bone_names = [x.name for x in armature_data.bones] @@ -176,7 +178,8 @@ def import_psa(psa_reader: PsaReader, armature_object, options: PsaImportOptions fcurve_frame_data = sequence_data_matrix[:, bone_index, fcurve_index] last_written_datum = 0 for frame_index, datum in enumerate(fcurve_frame_data): - # If the f-curve data is not different enough to the last written frame, un-mark this data for writing. + # If the f-curve data is not different enough to the last written frame, + # un-mark this data for writing. if frame_index > 0 and abs(datum - last_written_datum) < threshold: keyframe_write_matrix[frame_index, bone_index, fcurve_index] = 0 else: @@ -217,9 +220,12 @@ def import_psa(psa_reader: PsaReader, armature_object, options: PsaImportOptions nla_track.strips.new(name=action.name, start=0, action=action) +empty_set = set() + + class PsaImportActionListItem(PropertyGroup): - action_name: StringProperty(options=set()) - is_selected: BoolProperty(default=False, options=set()) + action_name: StringProperty(options=empty_set) + is_selected: BoolProperty(default=False, options=empty_set) def load_psa_file(context): @@ -246,7 +252,7 @@ def on_psa_file_path_updated(property, context): class PsaBonePropertyGroup(PropertyGroup): - bone_name: StringProperty(options=set()) + bone_name: StringProperty(options=empty_set) class PsaDataPropertyGroup(PropertyGroup): @@ -255,37 +261,37 @@ class PsaDataPropertyGroup(PropertyGroup): class PsaImportPropertyGroup(PropertyGroup): - psa_file_path: StringProperty(default='', options=set(), update=on_psa_file_path_updated, name='PSA File Path') + 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) sequence_list_index: IntProperty(name='', default=0) should_clean_keys: BoolProperty(default=True, name='Clean Keyframes', description='Exclude unnecessary keyframes from being written to the actions', - options=set()) + options=empty_set) 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', - options=set()) + options=empty_set) should_stash: BoolProperty(default=False, name='Stash', description='Stash each imported action as a strip on a new non-contributing NLA track', - options=set()) - should_use_action_name_prefix: BoolProperty(default=False, name='Prefix Action Name', options=set()) - action_name_prefix: StringProperty(default='', name='Prefix', options=set()) - should_overwrite: BoolProperty(default=False, name='Reuse Existing Actions', options=set(), + options=empty_set) + should_use_action_name_prefix: BoolProperty(default=False, name='Prefix Action Name', options=empty_set) + action_name_prefix: StringProperty(default='', name='Prefix', options=empty_set) + should_overwrite: BoolProperty(default=False, name='Reuse Existing Actions', options=empty_set, description='If an action with a matching name already exists, the existing action will have it\'s data overwritten instead of a new action being created') - should_write_keyframes: BoolProperty(default=True, name='Keyframes', options=set()) - should_write_metadata: BoolProperty(default=True, name='Metadata', options=set(), + should_write_keyframes: BoolProperty(default=True, name='Keyframes', options=empty_set) + should_write_metadata: BoolProperty(default=True, name='Metadata', options=empty_set, description='Additional data will be written to the custom properties of the Action (e.g., frame rate)') sequence_filter_name: StringProperty(default='', options={'TEXTEDIT_UPDATE'}) - sequence_filter_is_selected: BoolProperty(default=False, options=set(), name='Only Show Selected', + sequence_filter_is_selected: BoolProperty(default=False, options=empty_set, name='Only Show Selected', description='Only show selected sequences') - sequence_use_filter_invert: BoolProperty(default=False, options=set()) + sequence_use_filter_invert: BoolProperty(default=False, options=empty_set) sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression', - description='Filter using regular expressions', options=set()) + description='Filter using regular expressions', options=empty_set) select_text: PointerProperty(type=bpy.types.Text) -def filter_sequences(pg: PsaImportPropertyGroup, sequences: bpy.types.bpy_prop_collection) -> List[int]: +def filter_sequences(pg: PsaImportPropertyGroup, sequences) -> List[int]: bitflag_filter_item = 1 << 30 flt_flags = [bitflag_filter_item] * len(sequences) @@ -319,8 +325,7 @@ def filter_sequences(pg: PsaImportPropertyGroup, sequences: bpy.types.bpy_prop_c return flt_flags -def get_visible_sequences(pg: PsaImportPropertyGroup, sequences: bpy.types.bpy_prop_collection) -> List[ - PsaImportActionListItem]: +def get_visible_sequences(pg: PsaImportPropertyGroup, sequences) -> List[PsaImportActionListItem]: bitflag_filter_item = 1 << 30 visible_sequences = [] for i, flag in enumerate(filter_sequences(pg, sequences)): @@ -330,26 +335,25 @@ def get_visible_sequences(pg: PsaImportPropertyGroup, sequences: bpy.types.bpy_p class PSA_UL_SequenceList(UIList): - - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index, flt_flag): row = layout.row(align=True) split = row.split(align=True, factor=0.75) column = split.row(align=True) column.alignment = 'LEFT' column.prop(item, 'is_selected', icon_only=True) - column.label(text=item.action_name) + column.label(text=getattr(item, 'action_name')) def draw_filter(self, context, layout): - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') row = layout.row() - subrow = row.row(align=True) - subrow.prop(pg, 'sequence_filter_name', text="") - subrow.prop(pg, 'sequence_use_filter_invert', text="", icon='ARROW_LEFTRIGHT') - subrow.prop(pg, 'sequence_use_filter_regex', text="", icon='SORTBYEXT') - subrow.prop(pg, 'sequence_filter_is_selected', text="", icon='CHECKBOX_HLT') + sub_row = row.row(align=True) + sub_row.prop(pg, 'sequence_filter_name', text="") + sub_row.prop(pg, 'sequence_use_filter_invert', text="", icon='ARROW_LEFTRIGHT') + sub_row.prop(pg, 'sequence_use_filter_regex', text="", icon='SORTBYEXT') + sub_row.prop(pg, 'sequence_filter_is_selected', text="", icon='CHECKBOX_HLT') def filter_items(self, context, data, property): - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') sequences = getattr(data, property) flt_flags = filter_sequences(pg, sequences) flt_neworder = bpy.types.UI_UL_list.sort_items_by_name(sequences, 'action_name') @@ -372,7 +376,7 @@ class PsaImportSequencesFromText(Operator): @classmethod def poll(cls, context): - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') return len(pg.sequence_list) > 0 def invoke(self, context, event): @@ -380,12 +384,12 @@ class PsaImportSequencesFromText(Operator): def draw(self, context): layout = self.layout - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') layout.label(icon='INFO', text='Each sequence name should be on a new line.') layout.prop(pg, 'select_text', text='') def execute(self, context): - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') if pg.select_text is None: self.report({'ERROR_INVALID_CONTEXT'}, 'No text block selected') return {'CANCELLED'} @@ -408,13 +412,13 @@ class PsaImportSequencesSelectAll(Operator): @classmethod def poll(cls, context): - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') visible_sequences = get_visible_sequences(pg, pg.sequence_list) has_unselected_actions = any(map(lambda action: not action.is_selected, visible_sequences)) return len(visible_sequences) > 0 and has_unselected_actions def execute(self, context): - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') visible_sequences = get_visible_sequences(pg, pg.sequence_list) for sequence in visible_sequences: sequence.is_selected = True @@ -429,13 +433,13 @@ class PsaImportSequencesDeselectAll(Operator): @classmethod def poll(cls, context): - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') visible_sequences = get_visible_sequences(pg, pg.sequence_list) has_selected_sequences = any(map(lambda sequence: sequence.is_selected, visible_sequences)) return len(visible_sequences) > 0 and has_selected_sequences def execute(self, context): - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') visible_sequences = get_visible_sequences(pg, pg.sequence_list) for sequence in visible_sequences: sequence.is_selected = False @@ -451,7 +455,7 @@ class PSA_PT_ImportPanel_Advanced(Panel): def draw(self, context): layout = self.layout - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') col = layout.column(heading="Options") col.use_property_split = True @@ -476,11 +480,11 @@ class PSA_PT_ImportPanel(Panel): @classmethod def poll(cls, context): - return context.object.type == 'ARMATURE' + return context.view_layer.objects.active.type == 'ARMATURE' def draw(self, context): layout = self.layout - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') row = layout.row(align=True) row.operator(PsaImportSelectFile.bl_idname, text='', icon='FILEBROWSER') @@ -552,7 +556,7 @@ class PsaImportSelectFile(Operator): filter_glob: bpy.props.StringProperty(default="*.psa", options={'HIDDEN'}) def execute(self, context): - context.scene.psa_import.psa_file_path = self.filepath + getattr(context.scene, 'psa_import').psa_file_path = self.filepath return {"FINISHED"} def invoke(self, context, event): @@ -568,14 +572,14 @@ class PsaImportOperator(Operator): @classmethod def poll(cls, context): - pg = context.scene.psa_import + 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 = context.scene.psa_import + 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] @@ -613,7 +617,7 @@ class PsaImportFileSelectOperator(Operator, ImportHelper): return {'RUNNING_MODAL'} def execute(self, context): - pg = context.scene.psa_import + pg = getattr(context.scene, 'psa_import') pg.psa_file_path = self.filepath return {'FINISHED'} diff --git a/io_scene_psk_psa/psa/reader.py b/io_scene_psk_psa/psa/reader.py index 25cf342..0f0fba8 100644 --- a/io_scene_psk_psa/psa/reader.py +++ b/io_scene_psk_psa/psa/reader.py @@ -8,8 +8,8 @@ from .data import * class PsaReader(object): """ This class reads the sequences and bone information immediately upon instantiation and holds onto a file handle. - The keyframe data is not read into memory upon instantiation due to it's potentially very large size. - To read the key data for a particular sequence, call `read_sequence_keys`. + The keyframe data is not read into memory upon instantiation due to its potentially very large size. + To read the key data for a particular sequence, call :read_sequence_keys. """ def __init__(self, path): @@ -38,7 +38,8 @@ class PsaReader(object): return matrix def read_sequence_keys(self, sequence_name: str) -> List[Psa.Key]: - """ Reads and returns the key data for a sequence. + """ + Reads and returns the key data for a sequence. :param sequence_name: The name of the sequence. :return: A list of Psa.Keys. @@ -60,7 +61,7 @@ class PsaReader(object): return keys @staticmethod - def _read_types(fp, data_class: ctypes.Structure, section: Section, data): + def _read_types(fp, data_class, section: Section, data): buffer_length = section.data_size * section.data_count buffer = fp.read(buffer_length) offset = 0 @@ -86,7 +87,7 @@ class PsaReader(object): # Skip keys on this pass. We will keep this file open and read from it as needed. self.keys_data_offset = fp.tell() fp.seek(section.data_size * section.data_count, 1) - elif section.name in [b'SCALEKEYS']: + elif section.name == b'SCALEKEYS': fp.seek(section.data_size * section.data_count, 1) else: raise RuntimeError(f'Unrecognized section "{section.name}"') diff --git a/io_scene_psk_psa/psk/exporter.py b/io_scene_psk_psa/psk/exporter.py index f5c1528..91c2566 100644 --- a/io_scene_psk_psa/psk/exporter.py +++ b/io_scene_psk_psa/psk/exporter.py @@ -1,7 +1,7 @@ from typing import Type -from bpy.props import BoolProperty, StringProperty, CollectionProperty, IntProperty, EnumProperty, PointerProperty -from bpy.types import Operator, PropertyGroup, UIList, Material +from bpy.props import BoolProperty, StringProperty, CollectionProperty, IntProperty, EnumProperty +from bpy.types import Operator, PropertyGroup, UIList from bpy_extras.io_utils import ExportHelper from .builder import build_psk, PskBuildOptions, get_psk_input_objects @@ -70,7 +70,7 @@ def is_bone_filter_mode_item_available(context, identifier): class PSK_UL_MaterialList(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): row = layout.row() - row.label(text=str(item.material_name), icon='MATERIAL') + row.label(text=str(getattr(item, 'material_name')), icon='MATERIAL') class MaterialListItem(PropertyGroup): @@ -108,11 +108,11 @@ class PskMaterialListItemMoveUp(Operator): @classmethod def poll(cls, context): - pg = context.scene.psk_export + pg = getattr(context.scene, 'psk_export') return pg.material_list_index > 0 def execute(self, context): - pg = context.scene.psk_export + pg = getattr(context.scene, 'psk_export') pg.material_list.move(pg.material_list_index, pg.material_list_index - 1) pg.material_list_index -= 1 return {"FINISHED"} @@ -126,11 +126,11 @@ class PskMaterialListItemMoveDown(Operator): @classmethod def poll(cls, context): - pg = context.scene.psk_export + pg = getattr(context.scene, 'psk_export') return pg.material_list_index < len(pg.material_list) - 1 def execute(self, context): - pg = context.scene.psk_export + pg = getattr(context.scene, 'psk_export') pg.material_list.move(pg.material_list_index, pg.material_list_index + 1) pg.material_list_index += 1 return {"FINISHED"} @@ -157,7 +157,7 @@ class PskExportOperator(Operator, ExportHelper): self.report({'ERROR_INVALID_CONTEXT'}, str(e)) return {'CANCELLED'} - pg = context.scene.psk_export + pg = getattr(context.scene, 'psk_export') # Populate bone groups list. populate_bone_group_list(input_objects.armature_object, pg.bone_group_list) @@ -178,8 +178,7 @@ class PskExportOperator(Operator, ExportHelper): def draw(self, context): layout = self.layout - scene = context.scene - pg = scene.psk_export + pg = getattr(context.scene, 'psk_export') layout.prop(pg, 'use_raw_mesh_data') @@ -220,6 +219,7 @@ class PskExportOperator(Operator, ExportHelper): try: psk = build_psk(context, options) export_psk(psk, self.filepath) + self.report({'INFO'}, f'PSK export successful') except RuntimeError as e: self.report({'ERROR_INVALID_CONTEXT'}, str(e)) return {'CANCELLED'} diff --git a/io_scene_psk_psa/psk/importer.py b/io_scene_psk_psa/psk/importer.py index 76f265a..7c1b50e 100644 --- a/io_scene_psk_psa/psk/importer.py +++ b/io_scene_psk_psa/psk/importer.py @@ -1,13 +1,13 @@ import os import sys from math import inf -from typing import Optional +from typing import Optional, List import bmesh import bpy import numpy as np from bpy.props import BoolProperty, EnumProperty, FloatProperty, StringProperty -from bpy.types import Operator, PropertyGroup +from bpy.types import Operator, PropertyGroup, VertexGroup from bpy_extras.io_utils import ImportHelper from mathutils import Quaternion, Vector, Matrix @@ -28,6 +28,24 @@ class PskImportOptions(object): self.bone_length = 1.0 +class ImportBone(object): + """ + Intermediate bone type for the purpose of construction. + """ + def __init__(self, index: int, psk_bone: Psk.Bone): + self.index: int = index + self.psk_bone: Psk.Bone = psk_bone + self.parent: Optional[ImportBone] = None + self.local_rotation: Quaternion = Quaternion() + self.local_translation: Vector = Vector() + self.world_rotation_matrix: Matrix = Matrix() + self.world_matrix: Matrix = Matrix() + self.vertex_group = None + self.orig_quat: Quaternion = Quaternion() + self.orig_loc: Vector = Vector() + self.post_quat: Quaternion = Quaternion() + + def import_psk(psk: Psk, context, options: PskImportOptions): armature_object = None @@ -49,21 +67,6 @@ def import_psk(psk: Psk, context, options: PskImportOptions): bpy.ops.object.mode_set(mode='EDIT') - # Intermediate bone type for the purpose of construction. - class ImportBone(object): - def __init__(self, index: int, psk_bone: Psk.Bone): - self.index: int = index - self.psk_bone: Psk.Bone = psk_bone - self.parent: Optional[ImportBone] = None - self.local_rotation: Quaternion = Quaternion() - self.local_translation: Vector = Vector() - self.world_rotation_matrix: Matrix = Matrix() - self.world_matrix: Matrix = Matrix() - self.vertex_group = None - self.orig_quat: Quaternion = Quaternion() - self.orig_loc: Vector = Vector() - self.post_quat: Quaternion = Quaternion() - import_bones = [] for bone_index, psk_bone in enumerate(psk.bones): @@ -213,7 +216,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions): # Get a list of all bones that have weights associated with them. vertex_group_bone_indices = set(map(lambda weight: weight.bone_index, psk.weights)) - vertex_groups = [None] * len(psk.bones) + vertex_groups: List[Optional[VertexGroup]] = [None] * len(psk.bones) for bone_index, psk_bone in map(lambda x: (x, psk.bones[x]), vertex_group_bone_indices): vertex_groups[bone_index] = mesh_object.vertex_groups.new(name=psk_bone.name.decode('windows-1252')) @@ -234,16 +237,19 @@ def import_psk(psk: Psk, context, options: PskImportOptions): pass +empty_set = set() + + class PskImportPropertyGroup(PropertyGroup): should_import_vertex_colors: BoolProperty( default=True, - options=set(), + options=empty_set, name='Vertex Colors', description='Import vertex colors from PSKX files, if available' ) vertex_color_space: EnumProperty( name='Vertex Color Space', - options=set(), + options=empty_set, description='The source vertex color space', default='SRGBA', items=( @@ -254,25 +260,25 @@ class PskImportPropertyGroup(PropertyGroup): should_import_vertex_normals: BoolProperty( default=True, name='Vertex Normals', - options=set(), + options=empty_set, description='Import vertex normals from PSKX files, if available' ) should_import_extra_uvs: BoolProperty( default=True, name='Extra UVs', - options=set(), + options=empty_set, description='Import extra UV maps from PSKX files, if available' ) should_import_mesh: BoolProperty( default=True, name='Import Mesh', - options=set(), + options=empty_set, description='Import mesh' ) should_import_skeleton: BoolProperty( default=True, name='Import Skeleton', - options=set(), + options=empty_set, description='Import skeleton' ) bone_length: FloatProperty( @@ -281,7 +287,7 @@ class PskImportPropertyGroup(PropertyGroup): step=100, soft_min=1.0, name='Bone Length', - options=set(), + options=empty_set, description='Length of the bones' ) @@ -300,7 +306,7 @@ class PskImportOperator(Operator, ImportHelper): default='') def execute(self, context): - pg = context.scene.psk_import + pg = getattr(context.scene, 'psk_import') psk = read_psk(self.filepath) @@ -319,7 +325,7 @@ class PskImportOperator(Operator, ImportHelper): return {'FINISHED'} def draw(self, context): - pg = context.scene.psk_import + pg = getattr(context.scene, 'psk_import') layout = self.layout layout.prop(pg, 'should_import_mesh') row = layout.column() diff --git a/io_scene_psk_psa/psk/reader.py b/io_scene_psk_psa/psk/reader.py index c68a1af..414a5f4 100644 --- a/io_scene_psk_psa/psk/reader.py +++ b/io_scene_psk_psa/psk/reader.py @@ -3,7 +3,7 @@ import ctypes from .data import * -def _read_types(fp, data_class: ctypes.Structure, section: Section, data): +def _read_types(fp, data_class, section: Section, data): buffer_length = section.data_size * section.data_count buffer = fp.read(buffer_length) offset = 0 diff --git a/io_scene_psk_psa/types.py b/io_scene_psk_psa/types.py index 08cde91..1ffde62 100644 --- a/io_scene_psk_psa/types.py +++ b/io_scene_psk_psa/types.py @@ -1,12 +1,14 @@ from bpy.props import StringProperty, IntProperty, BoolProperty -from bpy.types import PropertyGroup, UIList +from bpy.types import PropertyGroup, UIList, UILayout, Context, AnyType class PSX_UL_BoneGroupList(UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): + + def draw_item(self, context: Context, layout: UILayout, data: AnyType, item: AnyType, icon: int, + active_data: AnyType, active_property: str, index: int = 0, flt_flag: int = 0): row = layout.row() - row.prop(item, 'is_selected', text=item.name) - row.label(text=str(item.count), icon='BONE_DATA') + row.prop(item, 'is_selected', text=getattr(item, 'name')) + row.label(text=str(getattr(item, 'count')), icon='BONE_DATA') class BoneGroupListItem(PropertyGroup):