diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index 911c287..6feeea6 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -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'} diff --git a/io_scene_psk_psa/psk/import_/operators.py b/io_scene_psk_psa/psk/import_/operators.py index 79cb57e..25a05a9 100644 --- a/io_scene_psk_psa/psk/import_/operators.py +++ b/io_scene_psk_psa/psk/import_/operators.py @@ -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, ) diff --git a/io_scene_psk_psa/psk/importer.py b/io_scene_psk_psa/psk/importer.py index c2f3348..8b9bdbf 100644 --- a/io_scene_psk_psa/psk/importer.py +++ b/io_scene_psk_psa/psk/importer.py @@ -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 diff --git a/io_scene_psk_psa/psk/properties.py b/io_scene_psk_psa/psk/properties.py index faa96e1..6931358 100644 --- a/io_scene_psk_psa/psk/properties.py +++ b/io_scene_psk_psa/psk/properties.py @@ -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,