Added the ability to re-order materials when exporting multiple objects

This commit is contained in:
Colin Basnett
2024-05-27 20:51:17 -07:00
parent 2ff4a661f2
commit 972ea5deda
3 changed files with 132 additions and 56 deletions

View File

@@ -2,11 +2,9 @@ bl_info = {
'name': 'ASCII Scene Export (ASE)', 'name': 'ASCII Scene Export (ASE)',
'description': 'Export ASE (ASCII Scene Export) files', 'description': 'Export ASE (ASCII Scene Export) files',
'author': 'Colin Basnett (Darklight Games)', 'author': 'Colin Basnett (Darklight Games)',
'version': (2, 0, 0), 'version': (2, 1, 0),
'blender': (4, 0, 0), 'blender': (4, 1, 0),
'location': 'File > Import-Export', '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', 'tracker_url': 'https://github.com/DarklightGames/io_scene_ase/issues',
'support': 'COMMUNITY', 'support': 'COMMUNITY',
'category': 'Import-Export' 'category': 'Import-Export'
@@ -26,26 +24,27 @@ from . import builder
from . import writer from . import writer
from . import exporter from . import exporter
classes = ( classes = exporter.classes
exporter.ASE_OT_ExportOperator,
exporter.ASE_OT_ExportCollections,
)
def menu_func_export(self, context): 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_export.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_collections.bl_idname, text='ASCII Scene Export Collections (.ase)')
def register(): def register():
for cls in classes: for cls in classes:
bpy.utils.register_class(cls) 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) bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
def unregister(): def unregister():
bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
del bpy.types.Scene.ase_export
for cls in classes: for cls in classes:
bpy.utils.unregister_class(cls) bpy.utils.unregister_class(cls)

View File

@@ -18,6 +18,7 @@ class ASEBuilderOptions(object):
def __init__(self): def __init__(self):
self.scale = 1.0 self.scale = 1.0
self.use_raw_mesh_data = False self.use_raw_mesh_data = False
self.materials = []
class ASEBuilder(object): class ASEBuilder(object):
@@ -29,6 +30,9 @@ class ASEBuilder(object):
mesh_objects = [obj for obj in objects if obj.type == 'MESH'] mesh_objects = [obj for obj in objects if obj.type == 'MESH']
context.window_manager.progress_begin(0, len(mesh_objects)) 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): for object_index, selected_object in enumerate(mesh_objects):
# Evaluate the mesh after modifiers are applied # Evaluate the mesh after modifiers are applied
if options.use_raw_mesh_data: if options.use_raw_mesh_data:
@@ -78,13 +82,7 @@ class ASEBuilder(object):
for mesh_material_index, material in enumerate(selected_object.data.materials): for mesh_material_index, material in enumerate(selected_object.data.materials):
if material is None: if material is None:
raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{selected_object.name}\' cannot be empty') raise ASEBuilderError(f'Material slot {mesh_material_index + 1} for mesh \'{selected_object.name}\' cannot be empty')
try: material_indices.append(ase.materials.index(material))
# 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)
mesh_data.calc_loop_triangles() mesh_data.calc_loop_triangles()

View File

@@ -1,47 +1,126 @@
import os.path import os.path
import typing
from bpy_extras.io_utils import ExportHelper from bpy_extras.io_utils import ExportHelper
from bpy.props import StringProperty, EnumProperty, BoolProperty from bpy.props import StringProperty, BoolProperty, CollectionProperty, PointerProperty, IntProperty
from bpy.types import Operator from bpy.types import Operator, Material, PropertyGroup, UIList
from .builder import * from .builder import *
from .writer import * from .writer import *
class ASE_OT_ExportOperator(Operator, ExportHelper): class ASE_PG_material(PropertyGroup):
bl_idname = 'io_scene_ase.ase_export' # important since its how bpy.ops.import_test.some_data is constructed 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_label = 'Export ASE'
bl_space_type = 'PROPERTIES' bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW' bl_region_type = 'WINDOW'
filename_ext = '.ase' filename_ext = '.ase'
filter_glob: StringProperty( filter_glob: StringProperty(default="*.ase", options={'HIDDEN'}, maxlen=255)
default="*.ase", use_raw_mesh_data: BoolProperty(default=False, name='Raw Mesh Data', description='No modifiers will be evaluated as part of the exported mesh')
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): def draw(self, context):
layout = self.layout 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): def execute(self, context):
options = ASEBuilderOptions() options = ASEBuilderOptions()
options.scale = self.units_scale[self.units] options.scale = 1.0
options.use_raw_mesh_data = self.use_raw_mesh_data 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: try:
ase = ASEBuilder().build(context, options, context.selected_objects) ase = ASEBuilder().build(context, options, context.selected_objects)
ASEWriter().write(self.filepath, ase) ASEWriter().write(self.filepath, ase)
@@ -52,7 +131,7 @@ class ASE_OT_ExportOperator(Operator, ExportHelper):
return {'CANCELLED'} 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_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_label = 'Export Collections to ASE'
bl_space_type = 'PROPERTIES' bl_space_type = 'PROPERTIES'
@@ -63,29 +142,18 @@ class ASE_OT_ExportCollections(Operator, ExportHelper):
options={'HIDDEN'}, options={'HIDDEN'},
maxlen=255, # Max internal buffer length, longer would be hilighted. 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( use_raw_mesh_data: BoolProperty(
default=False, default=False,
description='No modifiers will be evaluated as part of the exported mesh', description='No modifiers will be evaluated as part of the exported mesh',
name='Raw Mesh Data') name='Raw Mesh Data')
units_scale = {
'M': 60.352,
'U': 1.0
}
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.prop(self, 'units', expand=False)
layout.prop(self, 'use_raw_mesh_data') layout.prop(self, 'use_raw_mesh_data')
def execute(self, context): def execute(self, context):
options = ASEBuilderOptions() options = ASEBuilderOptions()
options.scale = self.units_scale[self.units] options.scale = 1.0
options.use_raw_mesh_data = self.use_raw_mesh_data options.use_raw_mesh_data = self.use_raw_mesh_data
# Iterate over all the visible collections in the scene. # 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') self.report({'INFO'}, f'{len(collections)} collections exported successfully')
return {'FINISHED'} 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,
)