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] = []
selected_armature_objects = [obj for obj in context.selected_objects if obj.type == 'ARMATURE']
match pg.sequence_source:
case 'ACTIONS':
for action_item in filter(lambda x: x.is_selected, pg.action_list):
@@ -507,6 +505,10 @@ class PSA_OT_export(Operator, ExportHelper):
case _:
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.animation_data = animation_data
options.sequences = export_sequences
@@ -530,9 +532,6 @@ class PSA_OT_export(Operator, ExportHelper):
write_psa(psa, self.filepath)
if len(psa.sequences) == 0:
self.report({'WARNING'}, 'No sequences were selected for export')
return {'FINISHED'}

View File

@@ -1,7 +1,8 @@
import os
from pathlib import Path
from bpy.props import StringProperty
from bpy.types import Operator, FileHandler, Context
from bpy.props import StringProperty, CollectionProperty
from bpy.types import Operator, FileHandler, Context, OperatorFileListElement, UILayout
from bpy_extras.io_utils import ImportHelper
from ..importer import PskImportOptions, import_psk
@@ -10,6 +11,70 @@ from ..reader import read_psk
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):
bl_idname = 'psk.import'
@@ -26,80 +91,69 @@ class PSK_OT_import(Operator, ImportHelper, PskImportMixin):
def execute(self, context):
psk = read_psk(self.filepath)
options = PskImportOptions()
options.name = os.path.splitext(os.path.basename(self.filepath))[0]
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)
name = os.path.splitext(os.path.basename(self.filepath))[0]
options = get_psk_import_options_from_properties(self)
result = import_psk(psk, context, name, options)
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)
self.report({'WARNING'}, message)
else:
self.report({'INFO'}, f'PSK imported ({options.name})')
self.report({'INFO'}, f'PSK imported as "{result.root_object.name}"')
return {'FINISHED'}
def draw(self, context):
layout = self.layout
psk_import_draw(self.layout, self)
row = layout.row()
col = row.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(self, 'scale')
class PSK_OT_import_drag_and_drop(Operator, PskImportMixin):
bl_idname = 'psk.import_drag_and_drop'
bl_label = 'Import Drag and Drop'
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)
mesh_header.prop(self, 'should_import_mesh')
directory: StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE', 'HIDDEN'})
files: CollectionProperty(type=OperatorFileListElement, options={'SKIP_SAVE', 'HIDDEN'})
if mesh_panel and self.should_import_mesh:
row = mesh_panel.row()
col = row.column()
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')
@classmethod
def poll(cls, context):
return context.area and context.area.type == 'VIEW_3D'
skeleton_header, skeleton_panel = layout.panel('skeleton_panel_id', default_closed=False)
skeleton_header.prop(self, 'should_import_skeleton')
def draw(self, context):
psk_import_draw(self.layout, self)
if skeleton_panel and self.should_import_skeleton:
row = skeleton_panel.row()
col = row.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(self, 'bone_length')
def invoke(self, context, event):
context.window_manager.invoke_props_dialog(self)
return {'RUNNING_MODAL'}
def execute(self, context):
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
class PSK_FH_import(FileHandler):
bl_idname = 'PSK_FH_import'
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_file_extensions = '.psk;.pskx'
@@ -107,7 +161,9 @@ class PSK_FH_import(FileHandler):
def poll_drop(cls, context: Context):
return context.area and context.area.type == 'VIEW_3D'
classes = (
PSK_OT_import,
PSK_OT_import_drag_and_drop,
PSK_FH_import,
)

View File

@@ -3,7 +3,7 @@ from typing import Optional, List
import bmesh
import bpy
import numpy as np
from bpy.types import VertexGroup
from bpy.types import VertexGroup, Context, Object
from mathutils import Quaternion, Vector, Matrix
from .data import Psk
@@ -13,7 +13,6 @@ from ..shared.helpers import rgb_to_srgb, is_bdk_addon_loaded
class PskImportOptions:
def __init__(self):
self.name = ''
self.should_import_mesh = True
self.should_reuse_materials = True
self.should_import_vertex_colors = True
@@ -49,17 +48,23 @@ class ImportBone:
class PskImportResult:
def __init__(self):
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()
armature_object = None
mesh_object = None
if options.should_import_skeleton:
# ARMATURE
armature_data = bpy.data.armatures.new(options.name)
armature_object = bpy.data.objects.new(options.name, armature_data)
armature_data = bpy.data.armatures.new(name)
armature_object = bpy.data.objects.new(name, armature_data)
armature_object.show_in_front = True
context.scene.collection.objects.link(armature_object)
@@ -116,8 +121,8 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
# MESH
if options.should_import_mesh:
mesh_data = bpy.data.meshes.new(options.name)
mesh_object = bpy.data.objects.new(options.name, mesh_data)
mesh_data = bpy.data.meshes.new(name)
mesh_object = bpy.data.objects.new(name, mesh_data)
# MATERIALS
if options.should_import_materials:
@@ -280,4 +285,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult:
except:
pass
result.armature_object = armature_object
result.mesh_object = mesh_object
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()
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:
should_import_vertex_colors: BoolProperty(
default=True,
@@ -76,11 +83,20 @@ class PskImportMixin:
options=empty_set,
description='Import extra UV maps, if available'
)
should_import_mesh: BoolProperty(
default=True,
name='Import Mesh',
import_components: EnumProperty(
name='Import Components',
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(
default=True,
@@ -88,10 +104,8 @@ class PskImportMixin:
options=empty_set,
)
should_import_skeleton: BoolProperty(
default=True,
name='Import Skeleton',
options=empty_set,
description='Import skeleton'
get=should_import_skleton_get,
)
bone_length: FloatProperty(
default=1.0,