1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2025-02-22 03:59:29 +01:00

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
This commit is contained in:
Colin Basnett 2024-12-27 14:59:22 -08:00
parent ff5ded004a
commit c1d5a2229d
5 changed files with 249 additions and 151 deletions

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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)

View File

@ -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