From c1d5a2229dec89d503a4482904deb9ab55721e05 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Fri, 27 Dec 2024 14:59:22 -0800 Subject: [PATCH] Unified handling of translating bpy PSK options to the options passed into the build function Also fixed a bug where the normal export operator was exporting duplicate objects --- io_scene_psk_psa/psk/export/operators.py | 253 ++++++++++++---------- io_scene_psk_psa/psk/export/properties.py | 128 ++++++++--- io_scene_psk_psa/psk/export/ui.py | 8 +- io_scene_psk_psa/shared/dfs.py | 3 +- io_scene_psk_psa/shared/ui.py | 8 +- 5 files changed, 249 insertions(+), 151 deletions(-) diff --git a/io_scene_psk_psa/psk/export/operators.py b/io_scene_psk_psa/psk/export/operators.py index 342ff7e..e948237 100644 --- a/io_scene_psk_psa/psk/export/operators.py +++ b/io_scene_psk_psa/psk/export/operators.py @@ -1,24 +1,23 @@ -from typing import List, Optional, cast +from typing import List, Optional, cast, Iterable import bpy -from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty -from bpy.types import Operator, Context, Object, Collection, SpaceProperties +from bpy.props import StringProperty +from bpy.types import Operator, Context, Object, Collection, SpaceProperties, Depsgraph, Material from bpy_extras.io_utils import ExportHelper -from .properties import object_eval_state_items, export_space_items +from .properties import add_psk_export_properties 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 -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]): +def get_materials_for_mesh_objects(depsgraph: Depsgraph, mesh_objects: Iterable[Object]): materials = [] for mesh_object in mesh_objects: - for i, material_slot in enumerate(mesh_object.material_slots): + evaluated_mesh_object = mesh_object.evaluated_get(depsgraph) + for i, material_slot in enumerate(evaluated_mesh_object.material_slots): material = material_slot.material if material is None: raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')') @@ -27,12 +26,12 @@ def get_materials_for_mesh_objects(mesh_objects: List[Object]): return materials -def populate_material_list(mesh_objects, material_list): - materials = get_materials_for_mesh_objects(mesh_objects) +def populate_material_name_list(depsgraph: Depsgraph, mesh_objects, material_list): + materials = get_materials_for_mesh_objects(depsgraph, mesh_objects) material_list.clear() for index, material in enumerate(materials): m = material_list.add() - m.material = material + m.material_name = material.name m.index = index @@ -79,6 +78,27 @@ class PSK_OT_populate_bone_collection_list(Operator): return {'FINISHED'} +class PSK_OT_populate_material_name_list(Operator): + bl_idname = 'psk_export.populate_material_name_list' + bl_label = 'Populate Material Name List' + bl_description = 'Populate the material name list from the objects that will be used in this 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'} + depsgraph = context.evaluated_depsgraph_get() + input_objects = get_psk_input_objects_for_collection(context.collection) + try: + populate_material_name_list(depsgraph, [x.obj for x in input_objects.mesh_objects], export_operator.material_name_list) + except RuntimeError as e: + self.report({'ERROR_INVALID_CONTEXT'}, str(e)) + return {'CANCELLED'} + return {'FINISHED'} + + class PSK_OT_material_list_move_up(Operator): bl_idname = 'psk_export.material_list_item_move_up' bl_label = 'Move Up' @@ -88,12 +108,12 @@ class PSK_OT_material_list_move_up(Operator): @classmethod def poll(cls, context): pg = getattr(context.scene, 'psk_export') - return pg.material_list_index > 0 + return pg.material_name_list_index > 0 def execute(self, context): pg = getattr(context.scene, 'psk_export') - pg.material_list.move(pg.material_list_index, pg.material_list_index - 1) - pg.material_list_index -= 1 + pg.material_name_list.move(pg.material_name_list_index, pg.material_name_list_index - 1) + pg.material_name_list_index -= 1 return {'FINISHED'} @@ -106,47 +126,97 @@ class PSK_OT_material_list_move_down(Operator): @classmethod def poll(cls, context): pg = getattr(context.scene, 'psk_export') - return pg.material_list_index < len(pg.material_list) - 1 + return pg.material_name_list_index < len(pg.material_name_list) - 1 def execute(self, context): pg = getattr(context.scene, 'psk_export') - pg.material_list.move(pg.material_list_index, pg.material_list_index + 1) - pg.material_list_index += 1 + pg.material_name_list.move(pg.material_name_list_index, pg.material_name_list_index + 1) + pg.material_name_list_index += 1 + return {'FINISHED'} + + +class PSK_OT_material_list_name_move_up(Operator): + bl_idname = 'psk_export.material_name_list_item_move_up' + bl_label = 'Move Up' + bl_options = {'INTERNAL'} + bl_description = 'Move the selected material name up one slot' + + @classmethod + def poll(cls, context): + export_operator = get_collection_export_operator_from_context(context) + if export_operator is None: + return False + return export_operator.material_name_list_index > 0 + + 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'} + export_operator.material_name_list.move(export_operator.material_name_list_index, export_operator.material_name_list_index - 1) + export_operator.material_name_list_index -= 1 + return {'FINISHED'} + + +class PSK_OT_material_list_name_move_down(Operator): + bl_idname = 'psk_export.material_name_list_item_move_down' + bl_label = 'Move Down' + bl_options = {'INTERNAL'} + bl_description = 'Move the selected material name down one slot' + + @classmethod + def poll(cls, context): + export_operator = get_collection_export_operator_from_context(context) + if export_operator is None: + return False + return export_operator.material_name_list_index < len(export_operator.material_name_list) - 1 + + 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'} + export_operator.material_name_list.move(export_operator.material_name_list_index, export_operator.material_name_list_index + 1) + export_operator.material_name_list_index += 1 return {'FINISHED'} empty_set = set() -axis_identifiers = ('X', 'Y', 'Z', '-X', '-Y', '-Z') -forward_items = ( - ('X', 'X Forward', ''), - ('Y', 'Y Forward', ''), - ('Z', 'Z Forward', ''), - ('-X', '-X Forward', ''), - ('-Y', '-Y Forward', ''), - ('-Z', '-Z Forward', ''), -) - -up_items = ( - ('X', 'X Up', ''), - ('Y', 'Y Up', ''), - ('Z', 'Z Up', ''), - ('-X', '-X Up', ''), - ('-Y', '-Y Up', ''), - ('-Z', '-Z Up', ''), -) - -def forward_axis_update(self, context): - if self.forward_axis == self.up_axis: - # Automatically set the up axis to the next available axis - self.up_axis = next((axis for axis in axis_identifiers if axis != self.forward_axis), 'Z') +def get_sorted_materials_by_names(materials: Iterable[Material], material_names: List[str]) -> List[Material]: + """ + Sorts the materials by the order of the material names list. Any materials not in the list will be appended to the + end of the list in the order they are found. + @param materials: A list of materials to sort + @param material_names: A list of material names to sort by + @return: A sorted list of materials + """ + materials_in_collection = [m for m in materials if m.name in material_names] + materials_not_in_collection = [m for m in materials if m.name not in material_names] + materials_in_collection = sorted(materials_in_collection, key=lambda x: material_names.index(x.name)) + return materials_in_collection + materials_not_in_collection -def up_axis_update(self, context): - if self.up_axis == self.forward_axis: - # Automatically set the forward axis to the next available axis - self.forward_axis = next((axis for axis in axis_identifiers if axis != self.up_axis), 'X') +def get_psk_build_options_from_property_group(mesh_objects: Iterable[Object], pg: 'PSK_PG_export', depsgraph: Optional[Depsgraph] = None) -> PskBuildOptions: + if depsgraph is None: + depsgraph = bpy.context.evaluated_depsgraph_get() + + options = PskBuildOptions() + options.object_eval_state = pg.object_eval_state + options.export_space = pg.export_space + options.bone_filter_mode = pg.bone_filter_mode + options.bone_collection_indices = [x.index for x in pg.bone_collection_list if x.is_selected] + options.scale = pg.scale + options.forward_axis = pg.forward_axis + options.up_axis = pg.up_axis + + # TODO: perhaps move this into the build function and replace the materials list with a material names list. + materials = get_materials_for_mesh_objects(depsgraph, mesh_objects) + options.materials = get_sorted_materials_by_names(materials, [m.material_name for m in pg.material_name_list]) + + return options + class PSK_OT_export_collection(Operator, ExportHelper): bl_idname = 'export.psk_collection' @@ -162,50 +232,6 @@ class PSK_OT_export_collection(Operator, ExportHelper): subtype='FILE_PATH') collection: StringProperty(options={'HIDDEN'}) - object_eval_state: EnumProperty( - items=object_eval_state_items, - name='Object Evaluation State', - default='EVALUATED' - ) - should_exclude_hidden_meshes: BoolProperty( - default=False, - name='Visible Only', - description='Export only visible meshes' - ) - scale: FloatProperty( - name='Scale', - default=1.0, - description='Scale factor to apply to the exported mesh and armature', - min=0.0001, - soft_max=100.0 - ) - export_space: EnumProperty( - name='Export Space', - description='Space to export the mesh in', - 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) - forward_axis: EnumProperty( - name='Forward', - items=forward_items, - default='X', - update=forward_axis_update - ) - up_axis: EnumProperty( - name='Up', - items=up_items, - default='Z', - update=up_axis_update - ) - def execute(self, context): collection = bpy.data.collections.get(self.collection) @@ -215,15 +241,7 @@ class PSK_OT_export_collection(Operator, ExportHelper): self.report({'ERROR_INVALID_CONTEXT'}, str(e)) return {'CANCELLED'} - options = PskBuildOptions() - 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.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] - options.forward_axis = self.forward_axis - options.up_axis = self.up_axis + options = get_psk_build_options_from_property_group([x.obj for x in input_objects.mesh_objects], self) try: result = build_psk(context, input_objects, options) @@ -261,12 +279,25 @@ 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: - bones_panel.operator(PSK_OT_populate_bone_collection_list.bl_idname, icon='FILE_REFRESH') draw_bone_filter_mode(bones_panel, self) if self.bone_filter_mode == 'BONE_COLLECTIONS': + bones_panel.operator(PSK_OT_populate_bone_collection_list.bl_idname, icon='FILE_REFRESH') rows = max(3, min(len(self.bone_collection_list), 10)) bones_panel.template_list('PSX_UL_bone_collection_list', '', self, 'bone_collection_list', self, 'bone_collection_list_index', rows=rows) + # MATERIALS + materials_header, materials_panel = layout.panel('Materials', default_closed=False) + materials_header.label(text='Materials', icon='MATERIAL') + + if materials_panel: + materials_panel.operator(PSK_OT_populate_material_name_list.bl_idname, icon='FILE_REFRESH') + rows = max(3, min(len(self.material_name_list), 10)) + row = materials_panel.row() + row.template_list('PSK_UL_material_names', '', self, 'material_name_list', self, 'material_name_list_index', rows=rows) + col = row.column(align=True) + col.operator(PSK_OT_material_list_name_move_up.bl_idname, text='', icon='TRIA_UP') + col.operator(PSK_OT_material_list_name_move_down.bl_idname, text='', icon='TRIA_DOWN') + # TRANSFORM transform_header, transform_panel = layout.panel('Transform', default_closed=False) transform_header.label(text='Transform') @@ -280,6 +311,11 @@ class PSK_OT_export_collection(Operator, ExportHelper): flow.prop(self, 'up_axis') + +add_psk_export_properties(PSK_OT_export_collection) + + + class PSK_OT_export(Operator, ExportHelper): bl_idname = 'export.psk' bl_label = 'Export' @@ -287,7 +323,6 @@ class PSK_OT_export(Operator, ExportHelper): bl_description = 'Export mesh and armature to PSK' filename_ext = '.psk' filter_glob: StringProperty(default='*.psk', options={'HIDDEN'}) - filepath: StringProperty( name='File Path', description='File path used for exporting the PSK file', @@ -309,8 +344,10 @@ class PSK_OT_export(Operator, ExportHelper): populate_bone_collection_list(input_objects.armature_object, pg.bone_collection_list) + depsgraph = context.evaluated_depsgraph_get() + try: - populate_material_list([x.obj for x in input_objects.mesh_objects], pg.material_list) + populate_material_name_list(depsgraph, [x.obj for x in input_objects.mesh_objects], pg.material_name_list) except RuntimeError as e: self.report({'ERROR_INVALID_CONTEXT'}, str(e)) return {'CANCELLED'} @@ -349,7 +386,7 @@ class PSK_OT_export(Operator, ExportHelper): if materials_panel: row = materials_panel.row() rows = max(3, min(len(pg.bone_collection_list), 10)) - row.template_list('PSK_UL_materials', '', pg, 'material_list', pg, 'material_list_index', rows=rows) + row.template_list('PSK_UL_material_names', '', pg, 'material_name_list', pg, 'material_name_list_index', rows=rows) col = row.column(align=True) col.operator(PSK_OT_material_list_move_up.bl_idname, text='', icon='TRIA_UP') col.operator(PSK_OT_material_list_move_down.bl_idname, text='', icon='TRIA_DOWN') @@ -358,15 +395,8 @@ class PSK_OT_export(Operator, ExportHelper): pg = getattr(context.scene, 'psk_export') input_objects = get_psk_input_objects_for_context(context) + options = get_psk_build_options_from_property_group([x.obj for x in input_objects.mesh_objects], pg) - options = PskBuildOptions() - options.bone_filter_mode = pg.bone_filter_mode - options.bone_collection_indices = [x.index for x in pg.bone_collection_list if x.is_selected] - options.object_eval_state = pg.object_eval_state - options.materials = [m.material for m in pg.material_list] - options.scale = pg.scale - options.export_space = pg.export_space - try: result = build_psk(context, input_objects, options) for warning in result.warnings: @@ -379,7 +409,7 @@ class PSK_OT_export(Operator, ExportHelper): except RuntimeError as e: self.report({'ERROR_INVALID_CONTEXT'}, str(e)) return {'CANCELLED'} - + return {'FINISHED'} @@ -389,4 +419,7 @@ classes = ( PSK_OT_export, PSK_OT_export_collection, PSK_OT_populate_bone_collection_list, + PSK_OT_populate_material_name_list, + PSK_OT_material_list_name_move_up, + PSK_OT_material_list_name_move_down, ) diff --git a/io_scene_psk_psa/psk/export/properties.py b/io_scene_psk_psa/psk/export/properties.py index c312188..189b82a 100644 --- a/io_scene_psk_psa/psk/export/properties.py +++ b/io_scene_psk_psa/psk/export/properties.py @@ -1,4 +1,5 @@ -from bpy.props import EnumProperty, CollectionProperty, IntProperty, PointerProperty, FloatProperty +from bpy.props import EnumProperty, CollectionProperty, IntProperty, PointerProperty, FloatProperty, StringProperty, \ + BoolProperty from bpy.types import PropertyGroup, Material from ...shared.data import bone_filter_mode_items @@ -6,7 +7,6 @@ from ...shared.types import PSX_PG_bone_collection_list_item empty_set = set() - object_eval_state_items = ( ('EVALUATED', 'Evaluated', 'Use data from fully evaluated object'), ('ORIGINAL', 'Original', 'Use data from original object with no modifiers applied'), @@ -17,44 +17,110 @@ export_space_items = [ ('ARMATURE', 'Armature', 'Export in armature space'), ] + +axis_identifiers = ('X', 'Y', 'Z', '-X', '-Y', '-Z') +forward_items = ( + ('X', 'X Forward', ''), + ('Y', 'Y Forward', ''), + ('Z', 'Z Forward', ''), + ('-X', '-X Forward', ''), + ('-Y', '-Y Forward', ''), + ('-Z', '-Z Forward', ''), +) + +up_items = ( + ('X', 'X Up', ''), + ('Y', 'Y Up', ''), + ('Z', 'Z Up', ''), + ('-X', '-X Up', ''), + ('-Y', '-Y Up', ''), + ('-Z', '-Z Up', ''), +) + class PSK_PG_material_list_item(PropertyGroup): material: PointerProperty(type=Material) index: IntProperty() +class PSK_PG_material_name_list_item(PropertyGroup): + material_name: StringProperty() + index: IntProperty() + + + + +def forward_axis_update(self, _context): + if self.forward_axis == self.up_axis: + # Automatically set the up axis to the next available axis + self.up_axis = next((axis for axis in axis_identifiers if axis != self.forward_axis), 'Z') + + +def up_axis_update(self, _context): + if self.up_axis == self.forward_axis: + # Automatically set the forward axis to the next available axis + self.forward_axis = next((axis for axis in axis_identifiers if axis != self.up_axis), 'X') + + + +# In order to share the same properties between the PSA and PSK export properties, we need to define the properties in a +# separate function and then apply them to the classes. This is because the collection exporter cannot have +# PointerProperties, so we must effectively duplicate the storage of the properties. +def add_psk_export_properties(cls): + cls.__annotations__['object_eval_state'] = EnumProperty( + items=object_eval_state_items, + name='Object Evaluation State', + default='EVALUATED' + ) + cls.__annotations__['should_exclude_hidden_meshes'] = BoolProperty( + default=False, + name='Visible Only', + description='Export only visible meshes' + ) + cls.__annotations__['scale'] = FloatProperty( + name='Scale', + default=1.0, + description='Scale factor to apply to the exported mesh and armature', + min=0.0001, + soft_max=100.0 + ) + cls.__annotations__['export_space'] = EnumProperty( + name='Export Space', + description='Space to export the mesh in', + items=export_space_items, + default='WORLD' + ) + cls.__annotations__['bone_filter_mode'] = EnumProperty( + name='Bone Filter', + options=empty_set, + description='', + items=bone_filter_mode_items, + ) + cls.__annotations__['bone_collection_list'] = CollectionProperty(type=PSX_PG_bone_collection_list_item) + cls.__annotations__['bone_collection_list_index'] = IntProperty(default=0) + cls.__annotations__['forward_axis'] = EnumProperty( + name='Forward', + items=forward_items, + default='X', + update=forward_axis_update + ) + cls.__annotations__['up_axis'] = EnumProperty( + name='Up', + items=up_items, + default='Z', + update=up_axis_update + ) + cls.__annotations__['material_name_list'] = CollectionProperty(type=PSK_PG_material_name_list_item) + cls.__annotations__['material_name_list_index'] = IntProperty(default=0) + class PSK_PG_export(PropertyGroup): - 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) - object_eval_state: EnumProperty( - items=object_eval_state_items, - name='Object Evaluation State', - default='EVALUATED' - ) - material_list: CollectionProperty(type=PSK_PG_material_list_item) - material_list_index: IntProperty(default=0) - scale: FloatProperty( - name='Scale', - default=1.0, - description='Scale factor to apply to the exported mesh', - min=0.0001, - soft_max=100.0 - ) - export_space: EnumProperty( - name='Export Space', - options=empty_set, - description='Space to export the mesh in', - items=export_space_items, - default='WORLD' - ) + pass + + +add_psk_export_properties(PSK_PG_export) classes = ( PSK_PG_material_list_item, + PSK_PG_material_name_list_item, PSK_PG_export, ) diff --git a/io_scene_psk_psa/psk/export/ui.py b/io_scene_psk_psa/psk/export/ui.py index 4fa55af..2412d19 100644 --- a/io_scene_psk_psa/psk/export/ui.py +++ b/io_scene_psk_psa/psk/export/ui.py @@ -1,12 +1,14 @@ +import bpy from bpy.types import UIList -class PSK_UL_materials(UIList): +class PSK_UL_material_names(UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): row = layout.row() - row.prop(item.material, 'name', text='', emboss=False, icon_value=layout.icon(item.material)) + material = bpy.data.materials.get(item.material_name, None) + row.prop(item, 'material_name', text='', emboss=False, icon_value=layout.icon(material) if material else 0) classes = ( - PSK_UL_materials, + PSK_UL_material_names, ) diff --git a/io_scene_psk_psa/shared/dfs.py b/io_scene_psk_psa/shared/dfs.py index d3c51ff..c1a05fd 100644 --- a/io_scene_psk_psa/shared/dfs.py +++ b/io_scene_psk_psa/shared/dfs.py @@ -133,11 +133,12 @@ def dfs_view_layer_objects(view_layer: ViewLayer) -> Iterable[DfsObject]: @param view_layer: The view layer to inspect. @return: An iterable of tuples containing the object, the instance objects, and the world matrix. ''' + visited = set() def layer_collection_objects_recursive(layer_collection: LayerCollection): for child in layer_collection.children: yield from layer_collection_objects_recursive(child) # Iterate only the top-level objects in this collection first. - yield from _dfs_collection_objects_recursive(layer_collection.collection) + yield from _dfs_collection_objects_recursive(layer_collection.collection, visited=visited) yield from layer_collection_objects_recursive(view_layer.layer_collection) diff --git a/io_scene_psk_psa/shared/ui.py b/io_scene_psk_psa/shared/ui.py index 0887b71..2eba27f 100644 --- a/io_scene_psk_psa/shared/ui.py +++ b/io_scene_psk_psa/shared/ui.py @@ -4,12 +4,8 @@ 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 + if identifier == 'BONE_COLLECTIONS' and len(pg.bone_collection_list) == 0: + return False return True