Impemented #120: Multiple PSKs can be imported at once with drag-and-drop

This commit is contained in:
Colin Basnett
2025-02-10 14:12:30 -08:00
parent d66cd09660
commit b855abaadb
4 changed files with 151 additions and 74 deletions

View File

@@ -455,8 +455,6 @@ class PSA_OT_export(Operator, ExportHelper):
export_sequences: List[PsaBuildSequence] = [] export_sequences: List[PsaBuildSequence] = []
selected_armature_objects = [obj for obj in context.selected_objects if obj.type == 'ARMATURE']
match pg.sequence_source: match pg.sequence_source:
case 'ACTIONS': case 'ACTIONS':
for action_item in filter(lambda x: x.is_selected, pg.action_list): for action_item in filter(lambda x: x.is_selected, pg.action_list):
@@ -507,6 +505,10 @@ class PSA_OT_export(Operator, ExportHelper):
case _: case _:
raise ValueError(f'Unhandled sequence source: {pg.sequence_source}') raise ValueError(f'Unhandled sequence source: {pg.sequence_source}')
if len(export_sequences) == 0:
self.report({'ERROR'}, 'No sequences were selected for export')
return {'CANCELLED'}
options = PsaBuildOptions() options = PsaBuildOptions()
options.animation_data = animation_data options.animation_data = animation_data
options.sequences = export_sequences options.sequences = export_sequences
@@ -530,9 +532,6 @@ class PSA_OT_export(Operator, ExportHelper):
write_psa(psa, self.filepath) write_psa(psa, self.filepath)
if len(psa.sequences) == 0:
self.report({'WARNING'}, 'No sequences were selected for export')
return {'FINISHED'} return {'FINISHED'}

View File

@@ -1,7 +1,8 @@
import os import os
from pathlib import Path
from bpy.props import StringProperty from bpy.props import StringProperty, CollectionProperty
from bpy.types import Operator, FileHandler, Context from bpy.types import Operator, FileHandler, Context, OperatorFileListElement, UILayout
from bpy_extras.io_utils import ImportHelper from bpy_extras.io_utils import ImportHelper
from ..importer import PskImportOptions, import_psk from ..importer import PskImportOptions, import_psk
@@ -10,6 +11,70 @@ from ..reader import read_psk
empty_set = set() empty_set = set()
def get_psk_import_options_from_properties(property_group: PskImportMixin):
options = PskImportOptions()
options.should_import_mesh = property_group.should_import_mesh
options.should_import_extra_uvs = property_group.should_import_extra_uvs
options.should_import_vertex_colors = property_group.should_import_vertex_colors
options.should_import_vertex_normals = property_group.should_import_vertex_normals
options.vertex_color_space = property_group.vertex_color_space
options.should_import_skeleton = property_group.should_import_skeleton
options.bone_length = property_group.bone_length
options.should_import_materials = property_group.should_import_materials
options.should_import_shape_keys = property_group.should_import_shape_keys
options.scale = property_group.scale
if property_group.bdk_repository_id:
options.bdk_repository_id = property_group.bdk_repository_id
return options
def psk_import_draw(layout: UILayout, props: PskImportMixin):
row = layout.row()
col = row.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(props, 'import_components')
if props.should_import_mesh:
mesh_header, mesh_panel = layout.panel('mesh_panel_id', default_closed=False)
mesh_header.label(text='Mesh', icon='MESH_DATA')
if mesh_panel:
row = mesh_panel.row()
col = row.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(props, 'should_import_materials', text='Materials')
col.prop(props, 'should_import_vertex_normals', text='Vertex Normals')
col.prop(props, 'should_import_extra_uvs', text='Extra UVs')
col.prop(props, 'should_import_vertex_colors', text='Vertex Colors')
if props.should_import_vertex_colors:
col.prop(props, 'vertex_color_space')
col.prop(props, 'should_import_shape_keys', text='Shape Keys')
if props.should_import_skeleton:
skeleton_header, skeleton_panel = layout.panel('skeleton_panel_id', default_closed=False)
skeleton_header.label(text='Skeleton', icon='OUTLINER_DATA_ARMATURE')
if skeleton_panel:
row = skeleton_panel.row()
col = row.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(props, 'bone_length')
transform_header, transform_panel = layout.panel('transform_panel_id', default_closed=False)
transform_header.label(text='Transform')
if transform_panel:
row = transform_panel.row()
col = row.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(props, 'scale')
class PSK_OT_import(Operator, ImportHelper, PskImportMixin): class PSK_OT_import(Operator, ImportHelper, PskImportMixin):
bl_idname = 'psk.import' bl_idname = 'psk.import'
@@ -26,80 +91,69 @@ class PSK_OT_import(Operator, ImportHelper, PskImportMixin):
def execute(self, context): def execute(self, context):
psk = read_psk(self.filepath) psk = read_psk(self.filepath)
name = os.path.splitext(os.path.basename(self.filepath))[0]
options = PskImportOptions() options = get_psk_import_options_from_properties(self)
options.name = os.path.splitext(os.path.basename(self.filepath))[0] result = import_psk(psk, context, name, options)
options.should_import_mesh = self.should_import_mesh
options.should_import_extra_uvs = self.should_import_extra_uvs
options.should_import_vertex_colors = self.should_import_vertex_colors
options.should_import_vertex_normals = self.should_import_vertex_normals
options.vertex_color_space = self.vertex_color_space
options.should_import_skeleton = self.should_import_skeleton
options.bone_length = self.bone_length
options.should_import_materials = self.should_import_materials
options.should_import_shape_keys = self.should_import_shape_keys
options.scale = self.scale
if self.bdk_repository_id:
options.bdk_repository_id = self.bdk_repository_id
if not options.should_import_mesh and not options.should_import_skeleton:
self.report({'ERROR'}, 'Nothing to import')
return {'CANCELLED'}
result = import_psk(psk, context, options)
if len(result.warnings): if len(result.warnings):
message = f'PSK imported with {len(result.warnings)} warning(s)\n' message = f'PSK imported as "{result.root_object.name}" with {len(result.warnings)} warning(s)\n'
message += '\n'.join(result.warnings) message += '\n'.join(result.warnings)
self.report({'WARNING'}, message) self.report({'WARNING'}, message)
else: else:
self.report({'INFO'}, f'PSK imported ({options.name})') self.report({'INFO'}, f'PSK imported as "{result.root_object.name}"')
return {'FINISHED'} return {'FINISHED'}
def draw(self, context): def draw(self, context):
layout = self.layout psk_import_draw(self.layout, self)
row = layout.row()
col = row.column() class PSK_OT_import_drag_and_drop(Operator, PskImportMixin):
col.use_property_split = True bl_idname = 'psk.import_drag_and_drop'
col.use_property_decorate = False bl_label = 'Import Drag and Drop'
col.prop(self, 'scale') bl_options = {'INTERNAL', 'UNDO', 'PRESET'}
bl_description = 'Import a PSK file by dragging and dropping it onto the 3D view'
mesh_header, mesh_panel = layout.panel('mesh_panel_id', default_closed=False) directory: StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE', 'HIDDEN'})
mesh_header.prop(self, 'should_import_mesh') files: CollectionProperty(type=OperatorFileListElement, options={'SKIP_SAVE', 'HIDDEN'})
if mesh_panel and self.should_import_mesh: @classmethod
row = mesh_panel.row() def poll(cls, context):
col = row.column() return context.area and context.area.type == 'VIEW_3D'
col.use_property_split = True
col.use_property_decorate = False
col.prop(self, 'should_import_materials', text='Materials')
col.prop(self, 'should_import_vertex_normals', text='Vertex Normals')
col.prop(self, 'should_import_extra_uvs', text='Extra UVs')
col.prop(self, 'should_import_vertex_colors', text='Vertex Colors')
if self.should_import_vertex_colors:
col.prop(self, 'vertex_color_space')
col.prop(self, 'should_import_shape_keys', text='Shape Keys')
skeleton_header, skeleton_panel = layout.panel('skeleton_panel_id', default_closed=False) def draw(self, context):
skeleton_header.prop(self, 'should_import_skeleton') psk_import_draw(self.layout, self)
if skeleton_panel and self.should_import_skeleton: def invoke(self, context, event):
row = skeleton_panel.row() context.window_manager.invoke_props_dialog(self)
col = row.column() return {'RUNNING_MODAL'}
col.use_property_split = True
col.use_property_decorate = False def execute(self, context):
col.prop(self, 'bone_length') warning_count = 0
options = get_psk_import_options_from_properties(self)
for file in self.files:
filepath = Path(self.directory) / file.name
psk = read_psk(filepath)
name = os.path.splitext(file.name)[0]
result = import_psk(psk, context, name, options)
if result.warnings:
warning_count += len(result.warnings)
if warning_count > 0:
self.report({'WARNING'}, f'Imported {len(self.files)} PSK file(s) with {warning_count} warning(s)')
else:
self.report({'INFO'}, f'Imported {len(self.files)} PSK file(s)')
return {'FINISHED'}
# TODO: move to another file # TODO: move to another file
class PSK_FH_import(FileHandler): class PSK_FH_import(FileHandler):
bl_idname = 'PSK_FH_import' bl_idname = 'PSK_FH_import'
bl_label = 'Unreal PSK' bl_label = 'Unreal PSK'
bl_import_operator = PSK_OT_import.bl_idname bl_import_operator = PSK_OT_import_drag_and_drop.bl_idname
bl_export_operator = 'psk.export_collection' bl_export_operator = 'psk.export_collection'
bl_file_extensions = '.psk;.pskx' bl_file_extensions = '.psk;.pskx'
@@ -107,7 +161,9 @@ class PSK_FH_import(FileHandler):
def poll_drop(cls, context: Context): def poll_drop(cls, context: Context):
return context.area and context.area.type == 'VIEW_3D' return context.area and context.area.type == 'VIEW_3D'
classes = ( classes = (
PSK_OT_import, PSK_OT_import,
PSK_OT_import_drag_and_drop,
PSK_FH_import, PSK_FH_import,
) )

View File

@@ -3,7 +3,7 @@ from typing import Optional, List
import bmesh import bmesh
import bpy import bpy
import numpy as np import numpy as np
from bpy.types import VertexGroup from bpy.types import VertexGroup, Context, Object
from mathutils import Quaternion, Vector, Matrix from mathutils import Quaternion, Vector, Matrix
from .data import Psk from .data import Psk
@@ -13,7 +13,6 @@ from ..shared.helpers import rgb_to_srgb, is_bdk_addon_loaded
class PskImportOptions: class PskImportOptions:
def __init__(self): def __init__(self):
self.name = ''
self.should_import_mesh = True self.should_import_mesh = True
self.should_reuse_materials = True self.should_reuse_materials = True
self.should_import_vertex_colors = True self.should_import_vertex_colors = True
@@ -49,17 +48,23 @@ class ImportBone:
class PskImportResult: class PskImportResult:
def __init__(self): def __init__(self):
self.warnings: List[str] = [] self.warnings: List[str] = []
self.armature_object: Optional[Object] = None
self.mesh_object: Optional[Object] = None
@property
def root_object(self) -> Object:
return self.armature_object if self.armature_object is not None else self.mesh_object
def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult: def import_psk(psk: Psk, context: Context, name: str, options: PskImportOptions) -> PskImportResult:
result = PskImportResult() result = PskImportResult()
armature_object = None armature_object = None
mesh_object = None mesh_object = None
if options.should_import_skeleton: if options.should_import_skeleton:
# ARMATURE # ARMATURE
armature_data = bpy.data.armatures.new(options.name) armature_data = bpy.data.armatures.new(name)
armature_object = bpy.data.objects.new(options.name, armature_data) armature_object = bpy.data.objects.new(name, armature_data)
armature_object.show_in_front = True armature_object.show_in_front = True
context.scene.collection.objects.link(armature_object) context.scene.collection.objects.link(armature_object)
@@ -116,8 +121,8 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
# MESH # MESH
if options.should_import_mesh: if options.should_import_mesh:
mesh_data = bpy.data.meshes.new(options.name) mesh_data = bpy.data.meshes.new(name)
mesh_object = bpy.data.objects.new(options.name, mesh_data) mesh_object = bpy.data.objects.new(name, mesh_data)
# MATERIALS # MATERIALS
if options.should_import_materials: if options.should_import_materials:
@@ -280,4 +285,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
except: except:
pass pass
result.armature_object = armature_object
result.mesh_object = mesh_object
return result return result

View File

@@ -47,6 +47,13 @@ def poly_flags_to_triangle_type_and_bit_flags(poly_flags: int) -> (str, set[str]
empty_set = set() empty_set = set()
def should_import_mesh_get(self):
return self.import_components in {'ALL', 'MESH'}
def should_import_skleton_get(self):
return self.import_components in {'ALL', 'SKELETON'}
class PskImportMixin: class PskImportMixin:
should_import_vertex_colors: BoolProperty( should_import_vertex_colors: BoolProperty(
default=True, default=True,
@@ -76,11 +83,20 @@ class PskImportMixin:
options=empty_set, options=empty_set,
description='Import extra UV maps, if available' description='Import extra UV maps, if available'
) )
should_import_mesh: BoolProperty( import_components: EnumProperty(
default=True, name='Import Components',
name='Import Mesh',
options=empty_set, options=empty_set,
description='Import mesh' description='Determine which components to import',
items=(
('ALL', 'Mesh & Skeleton', 'Import mesh and skeleton'),
('MESH', 'Mesh Only', 'Import mesh only'),
('SKELETON', 'Skeleton Only', 'Import skeleton only'),
),
default='ALL'
)
should_import_mesh: BoolProperty(
name='Import Mesh',
get=should_import_mesh_get,
) )
should_import_materials: BoolProperty( should_import_materials: BoolProperty(
default=True, default=True,
@@ -88,10 +104,8 @@ class PskImportMixin:
options=empty_set, options=empty_set,
) )
should_import_skeleton: BoolProperty( should_import_skeleton: BoolProperty(
default=True,
name='Import Skeleton', name='Import Skeleton',
options=empty_set, get=should_import_skleton_get,
description='Import skeleton'
) )
bone_length: FloatProperty( bone_length: FloatProperty(
default=1.0, default=1.0,