diff --git a/io_export_psk_psa/helpers.py b/io_export_psk_psa/helpers.py index 41f3c5b..34b74cb 100644 --- a/io_export_psk_psa/helpers.py +++ b/io_export_psk_psa/helpers.py @@ -9,11 +9,12 @@ def populate_bone_group_list(armature_object, bone_group_list): item.index = -1 item.is_selected = True - for bone_group_index, bone_group in enumerate(armature_object.pose.bone_groups): - item = bone_group_list.add() - item.name = bone_group.name - item.index = bone_group_index - item.is_selected = True + if armature_object and armature_object.pose: + for bone_group_index, bone_group in enumerate(armature_object.pose.bone_groups): + item = bone_group_list.add() + item.name = bone_group.name + item.index = bone_group_index + item.is_selected = True def add_bone_groups_to_layout(layout): diff --git a/io_export_psk_psa/psa/exporter.py b/io_export_psk_psa/psa/exporter.py index bd10552..3754b47 100644 --- a/io_export_psk_psa/psa/exporter.py +++ b/io_export_psk_psa/psa/exporter.py @@ -71,7 +71,7 @@ def is_bone_filter_mode_item_available(context, identifier): class PsaExportOperator(Operator, ExportHelper): bl_idname = 'export.psa' bl_label = 'Export' - __doc__ = 'PSA Exporter (.psa)' + __doc__ = 'Export actions to PSA' filename_ext = '.psa' filter_glob: StringProperty(default='*.psa', options={'HIDDEN'}) filepath: StringProperty( @@ -174,7 +174,11 @@ class PsaExportOperator(Operator, ExportHelper): options.bone_filter_mode = property_group.bone_filter_mode options.bone_group_indices = [x.index for x in property_group.bone_group_list if x.is_selected] builder = PsaBuilder() - psa = builder.build(context, options) + try: + psa = builder.build(context, options) + except RuntimeError as e: + self.report({'ERROR_INVALID_CONTEXT'}, str(e)) + return {'CANCELLED'} exporter = PsaExporter(psa) exporter.export(self.filepath) return {'FINISHED'} @@ -204,6 +208,7 @@ class PSA_UL_ExportActionList(UIList): class PsaExportSelectAll(bpy.types.Operator): bl_idname = 'psa_export.actions_select_all' bl_label = 'Select All' + bl_description = 'Select all actions' @classmethod def poll(cls, context): @@ -222,6 +227,7 @@ class PsaExportSelectAll(bpy.types.Operator): class PsaExportDeselectAll(bpy.types.Operator): bl_idname = 'psa_export.actions_deselect_all' bl_label = 'Deselect All' + bl_description = 'Deselect all actions' @classmethod def poll(cls, context): diff --git a/io_export_psk_psa/psa/importer.py b/io_export_psk_psa/psa/importer.py index 9d99131..620e24c 100644 --- a/io_export_psk_psa/psa/importer.py +++ b/io_export_psk_psa/psa/importer.py @@ -216,7 +216,7 @@ def on_armature_object_updated(property, context): class PsaImportPropertyGroup(bpy.types.PropertyGroup): - psa_file_path: StringProperty(default='', update=on_psa_file_path_updated) + psa_file_path: StringProperty(default='', update=on_psa_file_path_updated, name='PSA File Path') psa_bones: CollectionProperty(type=PsaImportPsaBoneItem) # armature_object: PointerProperty(name='Object', type=bpy.types.Object, update=on_armature_object_updated) action_list: CollectionProperty(type=PsaImportActionListItem) @@ -260,6 +260,7 @@ class PSA_UL_ImportActionList(UIList): class PsaImportSelectAll(bpy.types.Operator): bl_idname = 'psa_import.actions_select_all' bl_label = 'All' + bl_description = 'Select all actions' @classmethod def poll(cls, context): @@ -278,6 +279,7 @@ class PsaImportSelectAll(bpy.types.Operator): class PsaImportDeselectAll(bpy.types.Operator): bl_idname = 'psa_import.actions_deselect_all' bl_label = 'None' + bl_description = 'Deselect all actions' @classmethod def poll(cls, context): @@ -335,11 +337,12 @@ class PSA_PT_ImportPanel(Panel): class PsaImportSelectFile(Operator): - bl_idname = "psa_import.select_file" - bl_label = "Select" + 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"}) + 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): context.scene.psa_import.psa_file_path = self.filepath @@ -353,6 +356,7 @@ class PsaImportSelectFile(Operator): class PsaImportOperator(Operator): bl_idname = 'psa_import.import' bl_label = 'Import' + bl_description = 'Import the selected animations into the scene as actions' @classmethod def poll(cls, context): diff --git a/io_export_psk_psa/psk/builder.py b/io_export_psk_psa/psk/builder.py index f27bd12..c715f39 100644 --- a/io_export_psk_psa/psk/builder.py +++ b/io_export_psk_psa/psk/builder.py @@ -67,7 +67,8 @@ class PskBuilder(object): materials = OrderedDict() if armature_object is None: - # Static mesh (no armature) + # If the mesh has no armature object, simply assign it a dummy bone at the root to satisfy the requirement + # that a PSK file must have at least one bone. psk_bone = Psk.Bone() psk_bone.name = bytes('static', encoding='utf-8') psk_bone.flags = 0 @@ -79,13 +80,16 @@ class PskBuilder(object): else: bones = list(armature_object.data.bones) - # If bone groups are specified, get only the bones that are in the specified bone groups and their ancestors. - if len(options.bone_group_indices) > 0: + # If we are filtering by bone groups, get only the bones that are in the specified bone groups and their + # ancestors. + if options.bone_filter_mode == 'BONE_GROUPS': bone_indices = get_export_bone_indices_for_bone_groups(armature_object, options.bone_group_indices) bones = [bones[bone_index] for bone_index in bone_indices] # Ensure that the exported hierarchy has a single root bone. root_bones = [x for x in bones if x.parent is None] + print('root bones') + print(root_bones) if len(root_bones) > 1: root_bone_names = [x.name for x in bones] raise RuntimeError('Exported bone hierarchy must have a single root bone.' diff --git a/io_export_psk_psa/psk/exporter.py b/io_export_psk_psa/psk/exporter.py index 4e9b930..c9fdf90 100644 --- a/io_export_psk_psa/psk/exporter.py +++ b/io_export_psk_psa/psk/exporter.py @@ -64,7 +64,7 @@ def is_bone_filter_mode_item_available(context, identifier): input_objects = PskBuilder.get_input_objects(context) armature_object = input_objects.armature_object if identifier == 'BONE_GROUPS': - if not armature_object.pose or not armature_object.pose.bone_groups: + if not armature_object or not armature_object.pose or not armature_object.pose.bone_groups: return False # else if... you can set up other conditions if you add more options return True @@ -73,7 +73,7 @@ def is_bone_filter_mode_item_available(context, identifier): class PskExportOperator(Operator, ExportHelper): bl_idname = 'export.psk' bl_label = 'Export' - __doc__ = 'PSK Exporter (.psk)' + __doc__ = 'Export mesh and armature to PSK' filename_ext = '.psk' filter_glob: StringProperty(default='*.psk', options={'HIDDEN'}) @@ -125,7 +125,11 @@ class PskExportOperator(Operator, ExportHelper): builder = PskBuilder() options = PskBuilderOptions() options.bone_group_indices = [x.index for x in property_group.bone_group_list if x.is_selected] - psk = builder.build(context, options) + try: + psk = builder.build(context, options) + except RuntimeError as e: + self.report({'ERROR_INVALID_CONTEXT'}, str(e)) + return {'CANCELLED'} exporter = PskExporter(psk) exporter.export(self.filepath) return {'FINISHED'} diff --git a/io_export_psk_psa/psk/importer.py b/io_export_psk_psa/psk/importer.py index e651b0c..81283f8 100644 --- a/io_export_psk_psa/psk/importer.py +++ b/io_export_psk_psa/psk/importer.py @@ -167,7 +167,7 @@ class PskImporter(object): class PskImportOperator(Operator, ImportHelper): bl_idname = 'import.psk' bl_label = 'Export' - __doc__ = 'PSK Importer (.psk)' + __doc__ = 'Load a PSK file' filename_ext = '.psk' filter_glob: StringProperty(default='*.psk', options={'HIDDEN'}) filepath: StringProperty(