From 77dc4e5d504cd2755ba05c7baba8260e041eb5bf Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Fri, 29 Nov 2024 19:26:23 -0800 Subject: [PATCH] Added the ability to filter bone collections from PSK collection exporter --- io_scene_psk_psa/__init__.py | 3 + io_scene_psk_psa/psa/export/operators.py | 14 +-- io_scene_psk_psa/psa/export/properties.py | 7 +- io_scene_psk_psa/psk/export/operators.py | 102 ++++++++++++++++------ io_scene_psk_psa/psk/export/properties.py | 6 +- io_scene_psk_psa/shared/data.py | 6 ++ io_scene_psk_psa/shared/ui.py | 22 +++++ 7 files changed, 114 insertions(+), 46 deletions(-) create mode 100644 io_scene_psk_psa/shared/ui.py diff --git a/io_scene_psk_psa/__init__.py b/io_scene_psk_psa/__init__.py index 2eb6afa..124b498 100644 --- a/io_scene_psk_psa/__init__.py +++ b/io_scene_psk_psa/__init__.py @@ -6,6 +6,8 @@ if 'bpy' in locals(): importlib.reload(shared_data) importlib.reload(shared_helpers) importlib.reload(shared_types) + importlib.reload(shared_dfs) + importlib.reload(shared_ui) importlib.reload(psk_data) importlib.reload(psk_reader) @@ -33,6 +35,7 @@ if 'bpy' in locals(): importlib.reload(psa_import_ui) else: from .shared import data as shared_data, types as shared_types, helpers as shared_helpers + from .shared import dfs as shared_dfs, ui as shared_ui from .psk import data as psk_data, builder as psk_builder, writer as psk_writer, \ importer as psk_importer, properties as psk_properties from .psk import reader as psk_reader, ui as psk_ui diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index c374ee6..0e17939 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -13,6 +13,7 @@ from .properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_se from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions from ..writer import write_psa from ...shared.helpers import populate_bone_collection_list, get_nla_strips_in_frame_range +from ...shared.ui import draw_bone_filter_mode def is_action_for_armature(armature: Armature, action: Action): @@ -120,14 +121,6 @@ def get_animation_data_object(context: Context) -> Object: return animation_data_object -def is_bone_filter_mode_item_available(context, identifier): - if identifier == 'BONE_COLLECTIONS': - armature = context.active_object.data - if len(armature.collections) == 0: - return False - return True - - def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context: Context, marker_names: List[str]) -> Dict: # Timeline markers need to be sorted so that we can determine the sequence start and end positions. sequence_frame_ranges = dict() @@ -268,7 +261,7 @@ class PSA_OT_export(Operator, ExportHelper): propname, active_propname = get_sequences_propnames_from_source(pg.sequence_source) sequences_panel.template_list(PSA_UL_export_sequences.bl_idname, '', pg, propname, pg, active_propname, - rows=max(3, min(len(getattr(pg, propname), 10)))) + rows=max(3, min(len(getattr(pg, propname)), 10))) flow = sequences_panel.grid_flow() flow.use_property_split = True @@ -295,7 +288,8 @@ class PSA_OT_export(Operator, ExportHelper): bones_header.label(text='Bones', icon='BONE_DATA') if bones_panel: row = bones_panel.row(align=True) - row.prop(pg, 'bone_filter_mode', text='Bones') + + draw_bone_filter_mode(row, pg) if pg.bone_filter_mode == 'BONE_COLLECTIONS': row = bones_panel.row(align=True) diff --git a/io_scene_psk_psa/psa/export/properties.py b/io_scene_psk_psa/psa/export/properties.py index 35665e8..f473047 100644 --- a/io_scene_psk_psa/psa/export/properties.py +++ b/io_scene_psk_psa/psa/export/properties.py @@ -7,6 +7,7 @@ from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty StringProperty from bpy.types import PropertyGroup, Object, Action, AnimData, Context +from ...shared.data import bone_filter_mode_items from ...shared.types import PSX_PG_bone_collection_list_item @@ -155,11 +156,7 @@ class PSA_PG_export(PropertyGroup): name='Bone Filter', options=empty_set, description='', - items=( - ('ALL', 'All', 'All bones will be exported.'), - ('BONE_COLLECTIONS', 'Bone Collections', 'Only bones belonging to the selected bone collections and their ' - 'ancestors will be exported.'), - ) + items=bone_filter_mode_items, ) bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item) bone_collection_list_index: IntProperty(default=0, name='', description='') diff --git a/io_scene_psk_psa/psk/export/operators.py b/io_scene_psk_psa/psk/export/operators.py index 8f6dba4..30c687e 100644 --- a/io_scene_psk_psa/psk/export/operators.py +++ b/io_scene_psk_psa/psk/export/operators.py @@ -1,24 +1,18 @@ -from typing import List +from typing import List, Optional, cast import bpy -from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty -from bpy.types import Operator, Context, Object +from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty +from bpy.types import Operator, Context, Object, Collection, SpaceProperties from bpy_extras.io_utils import ExportHelper from .properties import object_eval_state_items, export_space_items from ..builder import build_psk, PskBuildOptions, get_psk_input_objects_for_context, \ get_psk_input_objects_for_collection from ..writer import write_psk +from ...shared.data import bone_filter_mode_items from ...shared.helpers import populate_bone_collection_list - - -def is_bone_filter_mode_item_available(context, identifier): - input_objects = get_psk_input_objects_for_context(context) - armature_object = input_objects.armature_object - if identifier == 'BONE_COLLECTIONS': - if armature_object is None or armature_object.data is None or len(armature_object.data.collections) == 0: - return False - return True +from ...shared.types import PSX_PG_bone_collection_list_item +from ...shared.ui import draw_bone_filter_mode def get_materials_for_mesh_objects(mesh_objects: List[Object]): @@ -42,6 +36,49 @@ def populate_material_list(mesh_objects, material_list): m.index = index + +def get_collection_from_context(context: Context) -> Optional[Collection]: + if context.space_data.type != 'PROPERTIES': + return None + + space_data = cast(SpaceProperties, context.space_data) + + if space_data.use_pin_id: + return cast(Collection, space_data.pin_id) + else: + return context.collection + + +def get_collection_export_operator_from_context(context: Context) -> Optional[object]: + collection = get_collection_from_context(context) + if collection is None: + return None + if 0 > collection.active_exporter_index >= len(collection.exporters): + return None + exporter = collection.exporters[collection.active_exporter_index] + # TODO: make sure this is actually an ASE exporter. + return exporter.export_properties + + +class PSK_OT_populate_bone_collection_list(Operator): + bl_idname = 'psk_export.populate_bone_collection_list' + bl_label = 'Populate Bone Collection List' + bl_description = 'Populate the bone collection list from the armature that will be used in this collection export' + bl_options = {'INTERNAL'} + + def execute(self, context): + export_operator = get_collection_export_operator_from_context(context) + if export_operator is None: + self.report({'ERROR_INVALID_CONTEXT'}, 'No valid export operator found in context') + return {'CANCELLED'} + input_objects = get_psk_input_objects_for_collection(context.collection) + if input_objects.armature_object is None: + self.report({'ERROR_INVALID_CONTEXT'}, 'No armature found in collection') + return {'CANCELLED'} + populate_bone_collection_list(input_objects.armature_object, export_operator.bone_collection_list) + return {'FINISHED'} + + class PSK_OT_material_list_move_up(Operator): bl_idname = 'psk_export.material_list_item_move_up' bl_label = 'Move Up' @@ -78,6 +115,9 @@ class PSK_OT_material_list_move_down(Operator): return {'FINISHED'} +empty_set = set() + + class PSK_OT_export_collection(Operator, ExportHelper): bl_idname = 'export.psk_collection' bl_label = 'Export' @@ -121,6 +161,14 @@ class PSK_OT_export_collection(Operator, ExportHelper): items=export_space_items, default='WORLD' ) + bone_filter_mode: EnumProperty( + name='Bone Filter', + options=empty_set, + description='', + items=bone_filter_mode_items, + ) + bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item) + bone_collection_list_index: IntProperty(default=0) def execute(self, context): @@ -133,12 +181,13 @@ class PSK_OT_export_collection(Operator, ExportHelper): return {'CANCELLED'} options = PskBuildOptions() - options.bone_filter_mode = 'ALL' options.object_eval_state = self.object_eval_state options.materials = get_materials_for_mesh_objects([x.obj for x in input_objects.mesh_objects]) options.should_enforce_bone_name_restrictions = self.should_enforce_bone_name_restrictions options.scale = self.scale options.export_space = self.export_space + options.bone_filter_mode = self.bone_filter_mode + options.bone_collection_indices = [x.index for x in self.bone_collection_list if x.is_selected] try: result = build_psk(context, input_objects, options) @@ -179,10 +228,17 @@ class PSK_OT_export_collection(Operator, ExportHelper): bones_header, bones_panel = layout.panel('Bones', default_closed=False) bones_header.label(text='Bones', icon='BONE_DATA') if bones_panel: - flow = bones_panel.grid_flow(row_major=True) - flow.use_property_split = True - flow.use_property_decorate = False - flow.prop(self, 'should_enforce_bone_name_restrictions') + draw_bone_filter_mode(bones_panel, self) + + row = bones_panel.row(align=True) + + if self.bone_filter_mode == 'BONE_COLLECTIONS': + rows = max(3, min(len(self.bone_collection_list), 10)) + row.template_list('PSX_UL_bone_collection_list', '', self, 'bone_collection_list', self, 'bone_collection_list_index', rows=rows) + col = row.column() + col.operator(PSK_OT_populate_bone_collection_list.bl_idname, text='', icon='FILE_REFRESH') + + bones_panel.prop(self, 'should_enforce_bone_name_restrictions') class PSK_OT_export(Operator, ExportHelper): @@ -215,7 +271,7 @@ class PSK_OT_export(Operator, ExportHelper): populate_bone_collection_list(input_objects.armature_object, pg.bone_collection_list) try: - populate_material_list([x[0] for x in input_objects.mesh_objects], pg.material_list) + populate_material_list([x.obj for x in input_objects.mesh_objects], pg.material_list) except RuntimeError as e: self.report({'ERROR_INVALID_CONTEXT'}, str(e)) return {'CANCELLED'} @@ -242,14 +298,7 @@ class PSK_OT_export(Operator, ExportHelper): bones_header, bones_panel = layout.panel('Bones', default_closed=False) bones_header.label(text='Bones', icon='BONE_DATA') if bones_panel: - bone_filter_mode_items = pg.bl_rna.properties['bone_filter_mode'].enum_items_static - row = bones_panel.row(align=True) - for item in bone_filter_mode_items: - identifier = item.identifier - item_layout = row.row(align=True) - item_layout.prop_enum(pg, 'bone_filter_mode', item.identifier) - item_layout.enabled = is_bone_filter_mode_item_available(context, identifier) - + draw_bone_filter_mode(bones_panel, pg) if pg.bone_filter_mode == 'BONE_COLLECTIONS': row = bones_panel.row() rows = max(3, min(len(pg.bone_collection_list), 10)) @@ -303,4 +352,5 @@ classes = ( PSK_OT_material_list_move_down, PSK_OT_export, PSK_OT_export_collection, + PSK_OT_populate_bone_collection_list, ) diff --git a/io_scene_psk_psa/psk/export/properties.py b/io_scene_psk_psa/psk/export/properties.py index 0506274..97bfb45 100644 --- a/io_scene_psk_psa/psk/export/properties.py +++ b/io_scene_psk_psa/psk/export/properties.py @@ -26,11 +26,7 @@ class PSK_PG_export(PropertyGroup): name='Bone Filter', options=empty_set, description='', - items=( - ('ALL', 'All', 'All bones will be exported'), - ('BONE_COLLECTIONS', 'Bone Collections', - 'Only bones belonging to the selected bone collections and their ancestors will be exported') - ) + items=bone_filter_mode_items ) bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item) bone_collection_list_index: IntProperty(default=0) diff --git a/io_scene_psk_psa/shared/data.py b/io_scene_psk_psa/shared/data.py index 0d3db93..9f29f1c 100644 --- a/io_scene_psk_psa/shared/data.py +++ b/io_scene_psk_psa/shared/data.py @@ -93,3 +93,9 @@ class Section(Structure): def __init__(self, *args, **kw): super().__init__(*args, **kw) self.type_flags = 1999801 + + +bone_filter_mode_items = ( + ('ALL', 'All', 'All bones will be exported'), + ('BONE_COLLECTIONS', 'Bone Collections', 'Only bones belonging to the selected bone collections and their ancestors will be exported') +) diff --git a/io_scene_psk_psa/shared/ui.py b/io_scene_psk_psa/shared/ui.py new file mode 100644 index 0000000..0887b71 --- /dev/null +++ b/io_scene_psk_psa/shared/ui.py @@ -0,0 +1,22 @@ +from bpy.types import UILayout + +from .data import bone_filter_mode_items + + +def is_bone_filter_mode_item_available(pg, identifier): + match identifier: + case 'BONE_COLLECTIONS': + if len(pg.bone_collection_list) == 0: + return False + case _: + pass + return True + + +def draw_bone_filter_mode(layout: UILayout, pg): + row = layout.row(align=True) + for item_identifier, _, _ in bone_filter_mode_items: + identifier = item_identifier + item_layout = row.row(align=True) + item_layout.prop_enum(pg, 'bone_filter_mode', item_identifier) + item_layout.enabled = is_bone_filter_mode_item_available(pg, identifier)