Added the ability to re-order materials when exporting multiple objects
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user