From 972ea5deda1471a33f8be7c65a177bede395162f Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Mon, 27 May 2024 20:51:17 -0700 Subject: [PATCH] Added the ability to re-order materials when exporting multiple objects --- io_scene_ase/__init__.py | 19 +++-- io_scene_ase/builder.py | 12 ++- io_scene_ase/exporter.py | 157 +++++++++++++++++++++++++++++---------- 3 files changed, 132 insertions(+), 56 deletions(-) diff --git a/io_scene_ase/__init__.py b/io_scene_ase/__init__.py index 40733c9..696d845 100644 --- a/io_scene_ase/__init__.py +++ b/io_scene_ase/__init__.py @@ -2,11 +2,9 @@ bl_info = { 'name': 'ASCII Scene Export (ASE)', 'description': 'Export ASE (ASCII Scene Export) files', 'author': 'Colin Basnett (Darklight Games)', - 'version': (2, 0, 0), - 'blender': (4, 0, 0), + 'version': (2, 1, 0), + 'blender': (4, 1, 0), 'location': 'File > Import-Export', - 'warning': 'This add-on is under development.', - 'wiki_url': 'https://github.com/DarklightGames/io_scene_ase/wiki', 'tracker_url': 'https://github.com/DarklightGames/io_scene_ase/issues', 'support': 'COMMUNITY', 'category': 'Import-Export' @@ -26,26 +24,27 @@ from . import builder from . import writer from . import exporter -classes = ( - exporter.ASE_OT_ExportOperator, - exporter.ASE_OT_ExportCollections, -) +classes = exporter.classes def menu_func_export(self, context): - self.layout.operator(exporter.ASE_OT_ExportOperator.bl_idname, text='ASCII Scene Export (.ase)') - self.layout.operator(exporter.ASE_OT_ExportCollections.bl_idname, text='ASCII Scene Export Collections (.ase)') + self.layout.operator(exporter.ASE_OT_export.bl_idname, text='ASCII Scene Export (.ase)') + self.layout.operator(exporter.ASE_OT_export_collections.bl_idname, text='ASCII Scene Export Collections (.ase)') def register(): for cls in classes: bpy.utils.register_class(cls) + bpy.types.Scene.ase_export = bpy.props.PointerProperty(type=exporter.ASE_PG_export) + bpy.types.TOPBAR_MT_file_export.append(menu_func_export) def unregister(): bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) + del bpy.types.Scene.ase_export + for cls in classes: bpy.utils.unregister_class(cls) diff --git a/io_scene_ase/builder.py b/io_scene_ase/builder.py index d11f969..b44ffe4 100644 --- a/io_scene_ase/builder.py +++ b/io_scene_ase/builder.py @@ -18,6 +18,7 @@ class ASEBuilderOptions(object): def __init__(self): self.scale = 1.0 self.use_raw_mesh_data = False + self.materials = [] class ASEBuilder(object): @@ -29,6 +30,9 @@ class ASEBuilder(object): mesh_objects = [obj for obj in objects if obj.type == 'MESH'] context.window_manager.progress_begin(0, len(mesh_objects)) + for material in options.materials: + ase.materials.append(material) + for object_index, selected_object in enumerate(mesh_objects): # Evaluate the mesh after modifiers are applied if options.use_raw_mesh_data: @@ -78,13 +82,7 @@ class ASEBuilder(object): for mesh_material_index, material in enumerate(selected_object.data.materials): if material is None: raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{selected_object.name}\' cannot be empty') - try: - # Reuse existing material entries for duplicates - material_index = ase.materials.index(material.name) - except ValueError: - material_index = len(ase.materials) - ase.materials.append(material.name) - material_indices.append(material_index) + material_indices.append(ase.materials.index(material)) mesh_data.calc_loop_triangles() diff --git a/io_scene_ase/exporter.py b/io_scene_ase/exporter.py index 2d9b8f2..9f2bf09 100644 --- a/io_scene_ase/exporter.py +++ b/io_scene_ase/exporter.py @@ -1,47 +1,126 @@ import os.path +import typing from bpy_extras.io_utils import ExportHelper -from bpy.props import StringProperty, EnumProperty, BoolProperty -from bpy.types import Operator +from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty +from bpy.types import Operator, Material, PropertyGroup, UIList from .builder import * from .writer import * -class ASE_OT_ExportOperator(Operator, ExportHelper): - bl_idname = 'io_scene_ase.ase_export' # important since its how bpy.ops.import_test.some_data is constructed +class ASE_PG_material(PropertyGroup): + material: PointerProperty(type=Material) + + +class ASE_PG_export(PropertyGroup): + material_list: CollectionProperty(name='Materials', type=ASE_PG_material) + material_list_index: IntProperty(name='Index', default=0) + + +def populate_material_list(mesh_objects, material_list): + material_list.clear() + + materials = [] + for mesh_object in mesh_objects: + for i, material_slot in enumerate(mesh_object.material_slots): + material = material_slot.material + if material is None: + raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')') + if material not in materials: + materials.append(material) + + for index, material in enumerate(materials): + m = material_list.add() + m.material = material + m.index = index + + +class ASE_OT_material_list_move_up(Operator): + bl_idname = 'ase_export.material_list_item_move_up' + bl_label = 'Move Up' + bl_options = {'INTERNAL'} + bl_description = 'Move the selected material up one slot' + + @classmethod + def poll(cls, context): + pg = getattr(context.scene, 'ase_export') + return pg.material_list_index > 0 + + def execute(self, context): + pg = getattr(context.scene, 'ase_export') + pg.material_list.move(pg.material_list_index, pg.material_list_index - 1) + pg.material_list_index -= 1 + return {'FINISHED'} + + +class ASE_OT_material_list_move_down(Operator): + bl_idname = 'ase_export.material_list_item_move_down' + bl_label = 'Move Down' + bl_options = {'INTERNAL'} + bl_description = 'Move the selected material down one slot' + + @classmethod + def poll(cls, context): + pg = getattr(context.scene, 'ase_export') + return pg.material_list_index < len(pg.material_list) - 1 + + def execute(self, context): + pg = getattr(context.scene, 'ase_export') + pg.material_list.move(pg.material_list_index, pg.material_list_index + 1) + pg.material_list_index += 1 + return {'FINISHED'} + + +class ASE_UL_materials(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)) + + + +class ASE_OT_export(Operator, ExportHelper): + bl_idname = 'io_scene_ase.ase_export' bl_label = 'Export ASE' bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' filename_ext = '.ase' - filter_glob: StringProperty( - default="*.ase", - options={'HIDDEN'}, - maxlen=255, # Max internal buffer length, longer would be hilighted. - ) - units: EnumProperty( - default='U', - items=(('M', 'Meters', ''), - ('U', 'Unreal', '')), - name='Units' - ) - use_raw_mesh_data: BoolProperty( - default=False, - description='No modifiers will be evaluated as part of the exported mesh', - name='Raw Mesh Data') - units_scale = { - 'M': 60.352, - 'U': 1.0 - } + filter_glob: StringProperty(default="*.ase", options={'HIDDEN'}, maxlen=255) + use_raw_mesh_data: BoolProperty(default=False, name='Raw Mesh Data', description='No modifiers will be evaluated as part of the exported mesh') def draw(self, context): layout = self.layout - layout.prop(self, 'units', expand=False) - layout.prop(self, 'use_raw_mesh_data') + + materials_header, materials_panel = layout.panel('Materials', default_closed=False) + materials_header.label(text='Materials') + + if materials_panel: + row = materials_panel.row() + row.template_list('ASE_UL_materials', '', context.scene.ase_export, 'material_list', context.scene.ase_export, 'material_list_index') + col = row.column(align=True) + col.operator(ASE_OT_material_list_move_up.bl_idname, icon='TRIA_UP', text='') + col.operator(ASE_OT_material_list_move_down.bl_idname, icon='TRIA_DOWN', text='') + + advanced_header, advanced_panel = layout.panel('Advanced', default_closed=True) + advanced_header.label(text='Advanced') + + if advanced_panel: + advanced_panel.prop(self, 'use_raw_mesh_data') + + def invoke(self, context: 'Context', event: 'Event' ) -> typing.Union[typing.Set[str], typing.Set[int]]: + mesh_objects = [x for x in context.selected_objects if x.type == 'MESH'] + pg = getattr(context.scene, 'ase_export') + populate_material_list(mesh_objects, pg.material_list) + + context.window_manager.fileselect_add(self) + + return {'RUNNING_MODAL'} def execute(self, context): options = ASEBuilderOptions() - options.scale = self.units_scale[self.units] + options.scale = 1.0 options.use_raw_mesh_data = self.use_raw_mesh_data + pg = getattr(context.scene, 'ase_export') + options.materials = [x.material for x in pg.material_list] try: ase = ASEBuilder().build(context, options, context.selected_objects) ASEWriter().write(self.filepath, ase) @@ -52,7 +131,7 @@ class ASE_OT_ExportOperator(Operator, ExportHelper): return {'CANCELLED'} -class ASE_OT_ExportCollections(Operator, ExportHelper): +class ASE_OT_export_collections(Operator, ExportHelper): bl_idname = 'io_scene_ase.ase_export_collections' # important since its how bpy.ops.import_test.some_data is constructed bl_label = 'Export Collections to ASE' bl_space_type = 'PROPERTIES' @@ -63,29 +142,18 @@ class ASE_OT_ExportCollections(Operator, ExportHelper): options={'HIDDEN'}, maxlen=255, # Max internal buffer length, longer would be hilighted. ) - units: EnumProperty( - default='U', - items=(('M', 'Meters', ''), - ('U', 'Unreal', '')), - name='Units' - ) use_raw_mesh_data: BoolProperty( default=False, description='No modifiers will be evaluated as part of the exported mesh', name='Raw Mesh Data') - units_scale = { - 'M': 60.352, - 'U': 1.0 - } def draw(self, context): layout = self.layout - layout.prop(self, 'units', expand=False) layout.prop(self, 'use_raw_mesh_data') def execute(self, context): options = ASEBuilderOptions() - options.scale = self.units_scale[self.units] + options.scale = 1.0 options.use_raw_mesh_data = self.use_raw_mesh_data # Iterate over all the visible collections in the scene. @@ -111,3 +179,14 @@ class ASE_OT_ExportCollections(Operator, ExportHelper): self.report({'INFO'}, f'{len(collections)} collections exported successfully') return {'FINISHED'} + + +classes = ( + ASE_PG_material, + ASE_UL_materials, + ASE_PG_export, + ASE_OT_export, + ASE_OT_export_collections, + ASE_OT_material_list_move_down, + ASE_OT_material_list_move_up, +)