Major overhaul to Python operator paths

Also started using mixin classes instead of ugly class annotation
ammendments. Various other clean-up items as well including using
generators where appropriate instead of returning full lists etc.

All the Python facing operators are now within `bpy.ops.psk` and
`bpy.ops.psa`. Before, they were scattered in a bunch of different
places because I wasn't really paying attention to consistency.
This commit is contained in:
Colin Basnett
2025-01-17 14:52:31 -08:00
parent 2b41815545
commit fec1a286ef
9 changed files with 327 additions and 292 deletions

View File

@@ -1,9 +1,9 @@
from collections import Counter
from typing import List, Iterable, Dict, Tuple, cast, Optional
from typing import List, Iterable, Dict, Tuple, Optional
import bpy
from bpy.props import StringProperty
from bpy.types import Context, Armature, Action, Object, AnimData, TimelineMarker
from bpy.types import Context, Action, Object, AnimData, TimelineMarker
from bpy_extras.io_utils import ExportHelper
from bpy_types import Operator
@@ -29,15 +29,22 @@ def get_sequences_propnames_from_source(sequence_source: str) -> Optional[Tuple[
raise ValueError(f'Unhandled sequence source: {sequence_source}')
def is_action_for_armature(armature: Armature, action: Action):
def is_action_for_object(obj: Object, action: Action):
if len(action.fcurves) == 0:
return False
if obj.animation_data is None:
return False
if obj.type != 'ARMATURE':
return False
version = SemanticVersion(bpy.app.version)
if version < SemanticVersion((4, 4, 0)):
import re
bone_names = set([x.name for x in armature.bones])
armature_data = obj.data
bone_names = set([x.name for x in armature_data.bones])
for fcurve in action.fcurves:
match = re.match(r'pose\.bones\[\"([^\"]+)\"](\[\"([^\"]+)\"])?', fcurve.data_path)
if not match:
@@ -46,12 +53,9 @@ def is_action_for_armature(armature: Armature, action: Action):
if bone_name in bone_names:
return True
else:
# Look up the armature by ID and check if its data block pointer matches the armature.
for slot in filter(lambda x: x.id_root == 'OBJECT', action.slots):
# Lop off the 'OB' prefix from the identifier for the lookup.
object = bpy.data.objects.get(slot.identifier[2:], None)
if object and object.data == armature:
return True
# In 4.4.0 and later, we can check if the object's action slot handle matches an action slot handle in the action.
if any(obj.animation_data.action_slot_handle == slot.handle for slot in action.slots):
return True
return False
@@ -71,22 +75,20 @@ def update_actions_and_timeline_markers(context: Context):
if animation_data is None:
return
active_armature = cast(Armature, context.active_object.data)
# Populate actions list.
for action in bpy.data.actions:
if not is_action_for_armature(active_armature, action):
if not is_action_for_object(context.active_object, action):
continue
if action.name != '' and not action.name.startswith('#'):
for (name, frame_start, frame_end) in get_sequences_from_action(action):
item = pg.action_list.add()
item.action = action
item.name = name
item.is_selected = False
item.is_pose_marker = False
item.frame_start = frame_start
item.frame_end = frame_end
for (name, frame_start, frame_end) in get_sequences_from_action(action):
print(name)
item = pg.action_list.add()
item.action = action
item.name = name
item.is_selected = False
item.is_pose_marker = False
item.frame_start = frame_start
item.frame_end = frame_end
# Pose markers are not guaranteed to be in frame-order, so make sure that they are.
pose_markers = sorted(action.pose_markers, key=lambda x: x.frame)
@@ -217,13 +219,15 @@ def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context:
return sequence_frame_ranges
def get_sequences_from_action(action: Action) -> List[Tuple[str, int, int]]:
def get_sequences_from_action(action: Action):
if action.name == '' or action.name.startswith('#'):
return
frame_start = int(action.frame_range[0])
frame_end = int(action.frame_range[1])
return get_sequences_from_name_and_frame_range(action.name, frame_start, frame_end)
yield from get_sequences_from_name_and_frame_range(action.name, frame_start, frame_end)
def get_sequences_from_action_pose_markers(action: Action, pose_markers: List[TimelineMarker], pose_marker: TimelineMarker, pose_marker_index: int) -> List[Tuple[str, int, int]]:
def get_sequences_from_action_pose_markers(action: Action, pose_markers: List[TimelineMarker], pose_marker: TimelineMarker, pose_marker_index: int):
frame_start = pose_marker.frame
sequence_name = pose_marker.name
if pose_marker.name.startswith('!'):
@@ -234,7 +238,7 @@ def get_sequences_from_action_pose_markers(action: Action, pose_markers: List[Ti
frame_end = pose_markers[pose_marker_index + 1].frame
else:
frame_end = int(action.frame_range[1])
return get_sequences_from_name_and_frame_range(sequence_name, frame_start, frame_end)
yield from get_sequences_from_name_and_frame_range(sequence_name, frame_start, frame_end)
def get_visible_sequences(pg: PSA_PG_export, sequences) -> List[PSA_PG_export_action_list_item]:
@@ -246,7 +250,7 @@ def get_visible_sequences(pg: PSA_PG_export, sequences) -> List[PSA_PG_export_ac
class PSA_OT_export(Operator, ExportHelper):
bl_idname = 'psa_export.operator'
bl_idname = 'psa.export'
bl_label = 'Export'
bl_options = {'INTERNAL', 'UNDO'}
bl_description = 'Export actions to PSA'
@@ -515,7 +519,7 @@ class PSA_OT_export(Operator, ExportHelper):
class PSA_OT_export_actions_select_all(Operator):
bl_idname = 'psa_export.sequences_select_all'
bl_idname = 'psa.export_actions_select_all'
bl_label = 'Select All'
bl_description = 'Select all visible sequences'
bl_options = {'INTERNAL'}
@@ -552,7 +556,7 @@ class PSA_OT_export_actions_select_all(Operator):
class PSA_OT_export_actions_deselect_all(Operator):
bl_idname = 'psa_export.sequences_deselect_all'
bl_idname = 'psa.export_sequences_deselect_all'
bl_label = 'Deselect All'
bl_description = 'Deselect all visible sequences'
bl_options = {'INTERNAL'}
@@ -587,7 +591,7 @@ class PSA_OT_export_actions_deselect_all(Operator):
class PSA_OT_export_bone_collections_select_all(Operator):
bl_idname = 'psa_export.bone_collections_select_all'
bl_idname = 'psa.export_bone_collections_select_all'
bl_label = 'Select All'
bl_description = 'Select all bone collections'
bl_options = {'INTERNAL'}
@@ -607,7 +611,7 @@ class PSA_OT_export_bone_collections_select_all(Operator):
class PSA_OT_export_bone_collections_deselect_all(Operator):
bl_idname = 'psa_export.bone_collections_deselect_all'
bl_idname = 'psa.export_bone_collections_deselect_all'
bl_label = 'Deselect All'
bl_description = 'Deselect all bone collections'
bl_options = {'INTERNAL'}

View File

@@ -1,7 +1,7 @@
import re
import sys
from fnmatch import fnmatch
from typing import List, Optional, Tuple
from typing import List, Optional
from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty, \
StringProperty
@@ -52,18 +52,16 @@ class PSA_PG_export_nla_strip_list_item(PropertyGroup):
is_selected: BoolProperty(default=True)
def get_sequences_from_name_and_frame_range(name: str, frame_start: int, frame_end: int) -> List[Tuple[str, int, int]]:
def get_sequences_from_name_and_frame_range(name: str, frame_start: int, frame_end: int):
reversed_pattern = r'(.+)/(.+)'
reversed_match = re.match(reversed_pattern, name)
if reversed_match:
forward_name = reversed_match.group(1)
backwards_name = reversed_match.group(2)
return [
(forward_name, frame_start, frame_end),
(backwards_name, frame_end, frame_start)
]
yield forward_name, frame_start, frame_end
yield backwards_name, frame_end, frame_start
else:
return [(name, frame_start, frame_end)]
yield name, frame_start, frame_end
def nla_track_update_cb(self: 'PSA_PG_export', context: Context) -> None:

View File

@@ -1,19 +1,27 @@
import os
from pathlib import Path
from typing import List
from typing import Iterable
from bpy.props import StringProperty, CollectionProperty
from bpy.types import Operator, Event, Context, FileHandler, OperatorFileListElement, Object
from bpy_extras.io_utils import ImportHelper
from .properties import get_visible_sequences
from .properties import get_visible_sequences, PsaImportMixin
from ..config import read_psa_config
from ..importer import import_psa, PsaImportOptions
from ..reader import PsaReader
class PSA_OT_import_sequences_from_text(Operator):
bl_idname = 'psa_import.sequences_select_from_text'
def psa_import_poll(cls, context: Context):
active_object = context.view_layer.objects.active
if active_object is None or active_object.type != 'ARMATURE':
cls.poll_message_set('The active object must be an armature')
return False
return True
class PSA_OT_import_sequences_select_from_text(Operator):
bl_idname = 'psa.import_sequences_select_from_text'
bl_label = 'Select By Text List'
bl_description = 'Select sequences by name from text list'
bl_options = {'INTERNAL', 'UNDO'}
@@ -49,7 +57,7 @@ class PSA_OT_import_sequences_from_text(Operator):
class PSA_OT_import_sequences_select_all(Operator):
bl_idname = 'psa_import.sequences_select_all'
bl_idname = 'psa.import_sequences_select_all'
bl_label = 'All'
bl_description = 'Select all sequences'
bl_options = {'INTERNAL'}
@@ -70,7 +78,7 @@ class PSA_OT_import_sequences_select_all(Operator):
class PSA_OT_import_sequences_deselect_all(Operator):
bl_idname = 'psa_import.sequences_deselect_all'
bl_idname = 'psa.import_sequences_deselect_all'
bl_label = 'None'
bl_description = 'Deselect all visible sequences'
bl_options = {'INTERNAL'}
@@ -113,8 +121,8 @@ def on_psa_file_path_updated(cls, context):
load_psa_file(context, cls.filepath)
class PSA_OT_import_multiple(Operator):
bl_idname = 'psa_import.import_multiple'
class PSA_OT_import_drag_and_drop(Operator, PsaImportMixin):
bl_idname = 'psa.import_drag_and_drop'
bl_label = 'Import PSA'
bl_description = 'Import multiple PSA files'
bl_options = {'INTERNAL', 'UNDO'}
@@ -122,23 +130,25 @@ class PSA_OT_import_multiple(Operator):
directory: StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE', 'HIDDEN'})
files: CollectionProperty(type=OperatorFileListElement, options={'SKIP_SAVE', 'HIDDEN'})
def execute(self, context):
pg = getattr(context.scene, 'psa_import')
warnings = []
sequence_names = []
for file in self.files:
psa_path = os.path.join(self.directory, file.name)
psa_path = str(os.path.join(self.directory, file.name))
psa_reader = PsaReader(psa_path)
sequence_names = list(psa_reader.sequences.keys())
file_sequence_names = list(psa_reader.sequences.keys())
options = psa_import_options_from_property_group(self, file_sequence_names)
result = _import_psa(context, pg, psa_path, sequence_names, context.view_layer.objects.active)
result.warnings.extend(warnings)
sequence_names.extend(file_sequence_names)
if len(result.warnings) > 0:
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
result = _import_psa(context, options, psa_path, context.view_layer.objects.active)
warnings.extend(result.warnings)
if len(warnings) > 0:
message = f'Imported {len(sequence_names)} action(s) with {len(warnings)} warning(s)\n'
self.report({'INFO'}, message)
for warning in result.warnings:
for warning in warnings:
self.report({'WARNING'}, warning)
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
@@ -146,7 +156,7 @@ class PSA_OT_import_multiple(Operator):
return {'FINISHED'}
def invoke(self, context: Context, event):
# Make sure the selected object is an armature.
# Make sure the selected object is an obj.
active_object = context.view_layer.objects.active
if active_object is None or active_object.type != 'ARMATURE':
self.report({'ERROR_INVALID_CONTEXT'}, 'The active object must be an armature')
@@ -158,18 +168,12 @@ class PSA_OT_import_multiple(Operator):
def draw(self, context):
layout = self.layout
pg = getattr(context.scene, 'psa_import')
draw_psa_import_options_no_panels(layout, pg)
draw_psa_import_options_no_panels(layout, self)
def _import_psa(context,
pg,
filepath: str,
sequence_names: List[str],
armature_object: Object
):
def psa_import_options_from_property_group(pg: PsaImportMixin, sequence_names: Iterable[str]) -> PsaImportOptions:
options = PsaImportOptions()
options.sequence_names = sequence_names
options.sequence_names = list(sequence_names)
options.should_use_fake_user = pg.should_use_fake_user
options.should_stash = pg.should_stash
options.action_name_prefix = pg.action_name_prefix if pg.should_use_action_name_prefix else ''
@@ -181,7 +185,14 @@ def _import_psa(context,
options.fps_source = pg.fps_source
options.fps_custom = pg.fps_custom
options.translation_scale = pg.translation_scale
return options
def _import_psa(context,
options: PsaImportOptions,
filepath: str,
armature_object: Object
):
warnings = []
if options.should_use_config_file:
@@ -189,7 +200,7 @@ def _import_psa(context,
config_path = Path(filepath).with_suffix('.config')
if config_path.exists():
try:
options.psa_config = read_psa_config(sequence_names, str(config_path))
options.psa_config = read_psa_config(options.sequence_names, str(config_path))
except Exception as e:
warnings.append(f'Failed to read PSA config file: {e}')
@@ -201,8 +212,8 @@ def _import_psa(context,
return result
class PSA_OT_import(Operator, ImportHelper):
bl_idname = 'psa_import.import'
class PSA_OT_import(Operator, ImportHelper, PsaImportMixin):
bl_idname = 'psa.import'
bl_label = 'Import'
bl_description = 'Import the selected animations into the scene as actions'
bl_options = {'INTERNAL', 'UNDO'}
@@ -218,29 +229,25 @@ class PSA_OT_import(Operator, ImportHelper):
@classmethod
def poll(cls, context):
active_object = context.view_layer.objects.active
if active_object is None or active_object.type != 'ARMATURE':
cls.poll_message_set('The active object must be an armature')
return False
return True
return psa_import_poll(cls, context)
def execute(self, context):
pg = getattr(context.scene, 'psa_import')
sequence_names = [x.action_name for x in pg.sequence_list if x.is_selected]
options = psa_import_options_from_property_group(self, [x.action_name for x in pg.sequence_list if x.is_selected])
if len(sequence_names) == 0:
if len(options.sequence_names) == 0:
self.report({'ERROR_INVALID_CONTEXT'}, 'No sequences selected')
return {'CANCELLED'}
result = _import_psa(context, pg, self.filepath, sequence_names, context.view_layer.objects.active)
result = _import_psa(context, options, self.filepath, context.view_layer.objects.active)
if len(result.warnings) > 0:
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
message = f'Imported {len(options.sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
self.report({'WARNING'}, message)
for warning in result.warnings:
self.report({'WARNING'}, warning)
else:
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
self.report({'INFO'}, f'Imported {len(options.sequence_names)} action(s)')
return {'FINISHED'}
@@ -271,7 +278,7 @@ class PSA_OT_import(Operator, ImportHelper):
row2 = col.row(align=True)
row2.label(text='Select')
row2.operator(PSA_OT_import_sequences_from_text.bl_idname, text='', icon='TEXT')
row2.operator(PSA_OT_import_sequences_select_from_text.bl_idname, text='', icon='TEXT')
row2.operator(PSA_OT_import_sequences_select_all.bl_idname, text='All', icon='CHECKBOX_HLT')
row2.operator(PSA_OT_import_sequences_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT')
@@ -281,13 +288,13 @@ class PSA_OT_import(Operator, ImportHelper):
col = sequences_panel.column(heading='')
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'fps_source')
if pg.fps_source == 'CUSTOM':
col.prop(pg, 'fps_custom')
col.prop(pg, 'should_overwrite')
col.prop(pg, 'should_use_action_name_prefix')
if pg.should_use_action_name_prefix:
col.prop(pg, 'action_name_prefix')
col.prop(self, 'fps_source')
if self.fps_source == 'CUSTOM':
col.prop(self, 'fps_custom')
col.prop(self, 'should_overwrite')
col.prop(self, 'should_use_action_name_prefix')
if self.should_use_action_name_prefix:
col.prop(self, 'action_name_prefix')
data_header, data_panel = layout.panel('data_panel_id', default_closed=False)
data_header.label(text='Data')
@@ -296,14 +303,14 @@ class PSA_OT_import(Operator, ImportHelper):
col = data_panel.column(heading='Write')
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'should_write_keyframes')
col.prop(pg, 'should_write_metadata')
col.prop(self, 'should_write_keyframes')
col.prop(self, 'should_write_metadata')
if pg.should_write_keyframes:
if self.should_write_keyframes:
col = col.column(heading='Keyframes')
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'should_convert_to_samples')
col.prop(self, 'should_convert_to_samples')
advanced_header, advanced_panel = layout.panel('advanced_panel_id', default_closed=True)
advanced_header.label(text='Advanced')
@@ -312,22 +319,22 @@ class PSA_OT_import(Operator, ImportHelper):
col = advanced_panel.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'bone_mapping_mode')
col.prop(self, 'bone_mapping_mode')
col = advanced_panel.column()
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'translation_scale', text='Translation Scale')
col.prop(self, 'translation_scale', text='Translation Scale')
col = advanced_panel.column(heading='Options')
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'should_use_fake_user')
col.prop(pg, 'should_stash')
col.prop(pg, 'should_use_config_file')
col.prop(self, 'should_use_fake_user')
col.prop(self, 'should_stash')
col.prop(self, 'should_use_config_file')
def draw_psa_import_options_no_panels(layout, pg):
def draw_psa_import_options_no_panels(layout, pg: PsaImportMixin):
col = layout.column(heading='Sequences')
col.use_property_split = True
col.use_property_decorate = False
@@ -365,11 +372,11 @@ def draw_psa_import_options_no_panels(layout, pg):
col.prop(pg, 'should_use_config_file')
class PSA_FH_import(FileHandler):
class PSA_FH_import(FileHandler): # TODO: rename and add handling for PSA export.
bl_idname = 'PSA_FH_import'
bl_label = 'File handler for Unreal PSA import'
bl_import_operator = 'psa_import.import_multiple'
bl_export_operator = 'psa_export.export'
bl_import_operator = PSA_OT_import_drag_and_drop.bl_idname
# bl_export_operator = 'psa_export.export'
bl_file_extensions = '.psa'
@classmethod
@@ -380,8 +387,8 @@ class PSA_FH_import(FileHandler):
classes = (
PSA_OT_import_sequences_select_all,
PSA_OT_import_sequences_deselect_all,
PSA_OT_import_sequences_from_text,
PSA_OT_import_sequences_select_from_text,
PSA_OT_import,
PSA_OT_import_multiple,
PSA_OT_import_drag_and_drop,
PSA_FH_import,
)

View File

@@ -23,21 +23,33 @@ class PSA_PG_data(PropertyGroup):
sequence_count: IntProperty(default=0)
class PSA_PG_import(PropertyGroup):
psa_error: StringProperty(default='')
psa: PointerProperty(type=PSA_PG_data)
sequence_list: CollectionProperty(type=PSA_PG_import_action_list_item)
sequence_list_index: IntProperty(name='', default=0)
bone_mapping_items = (
('EXACT', 'Exact', 'Bone names must match exactly.', 'EXACT', 0),
('CASE_INSENSITIVE', 'Case Insensitive', 'Bones names must match, ignoring case (e.g., the bone PSA bone \'root\' can be mapped to the armature bone \'Root\')', 'CASE_INSENSITIVE', 1),
)
fps_source_items = (
('SEQUENCE', 'Sequence', 'The sequence frame rate matches the original frame rate', 'ACTION', 0),
('SCENE', 'Scene', 'The sequence is resampled to the frame rate of the scene', 'SCENE_DATA', 1),
('CUSTOM', 'Custom', 'The sequence is resampled to a custom frame rate', 2),
)
compression_ratio_source_items = (
('ACTION', 'Action', 'The compression ratio is sourced from the action metadata', 'ACTION', 0),
('CUSTOM', 'Custom', 'The compression ratio is set to a custom value', 1),
)
class PsaImportMixin:
should_use_fake_user: BoolProperty(default=True, name='Fake User',
description='Assign each imported action a fake user so that the data block is '
'saved even it has no users',
options=empty_set)
should_use_config_file: BoolProperty(default=True, name='Use Config File',
description='Use the .config file that is sometimes generated when the PSA '
'file is exported from UEViewer. This file contains '
'options that can be used to filter out certain bones tracks '
'from the imported actions',
options=empty_set)
description='Use the .config file that is sometimes generated when the PSA '
'file is exported from UEViewer. This file contains '
'options that can be used to filter out certain bones tracks '
'from the imported actions',
options=empty_set)
should_stash: BoolProperty(default=False, name='Stash',
description='Stash each imported action as a strip on a new non-contributing NLA track',
options=empty_set)
@@ -56,7 +68,7 @@ class PSA_PG_import(PropertyGroup):
sequence_use_filter_invert: BoolProperty(default=False, options=empty_set)
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression',
description='Filter using regular expressions', options=empty_set)
select_text: PointerProperty(type=Text)
should_convert_to_samples: BoolProperty(
default=False,
name='Convert to Samples',
@@ -67,18 +79,10 @@ class PSA_PG_import(PropertyGroup):
name='Bone Mapping',
options=empty_set,
description='The method by which bones from the incoming PSA file are mapped to the armature',
items=(
('EXACT', 'Exact', 'Bone names must match exactly.', 'EXACT', 0),
('CASE_INSENSITIVE', 'Case Insensitive', 'Bones names must match, ignoring case (e.g., the bone PSA bone '
'\'root\' can be mapped to the armature bone \'Root\')', 'CASE_INSENSITIVE', 1),
),
items=bone_mapping_items,
default='CASE_INSENSITIVE'
)
fps_source: EnumProperty(name='FPS Source', items=(
('SEQUENCE', 'Sequence', 'The sequence frame rate matches the original frame rate', 'ACTION', 0),
('SCENE', 'Scene', 'The sequence is resampled to the frame rate of the scene', 'SCENE_DATA', 1),
('CUSTOM', 'Custom', 'The sequence is resampled to a custom frame rate', 2),
))
fps_source: EnumProperty(name='FPS Source', items=fps_source_items)
fps_custom: FloatProperty(
default=30.0,
name='Custom FPS',
@@ -89,10 +93,7 @@ class PSA_PG_import(PropertyGroup):
soft_max=60.0,
step=100,
)
compression_ratio_source: EnumProperty(name='Compression Ratio Source', items=(
('ACTION', 'Action', 'The compression ratio is sourced from the action metadata', 'ACTION', 0),
('CUSTOM', 'Custom', 'The compression ratio is set to a custom value', 1),
))
compression_ratio_source: EnumProperty(name='Compression Ratio Source', items=compression_ratio_source_items)
compression_ratio_custom: FloatProperty(
default=1.0,
name='Custom Compression Ratio',
@@ -110,6 +111,22 @@ class PSA_PG_import(PropertyGroup):
)
# This property group lives "globally" in the scene, since Operators cannot have PointerProperty or CollectionProperty
# properties.
class PSA_PG_import(PropertyGroup):
psa_error: StringProperty(default='')
psa: PointerProperty(type=PSA_PG_data)
sequence_list: CollectionProperty(type=PSA_PG_import_action_list_item)
sequence_list_index: IntProperty(name='', default=0)
sequence_filter_name: StringProperty(default='', options={'TEXTEDIT_UPDATE'})
sequence_filter_is_selected: BoolProperty(default=False, options=empty_set, name='Only Show Selected',
description='Only show selected sequences')
sequence_use_filter_invert: BoolProperty(default=False, options=empty_set)
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression',
description='Filter using regular expressions', options=empty_set)
select_text: PointerProperty(type=Text)
def filter_sequences(pg: PSA_PG_import, sequences) -> List[int]:
bitflag_filter_item = 1 << 30
flt_flags = [bitflag_filter_item] * len(sequences)

View File

@@ -12,21 +12,36 @@ from .reader import PsaReader
class PsaImportOptions(object):
def __init__(self):
self.should_use_fake_user = False
self.should_stash = False
self.sequence_names = []
self.should_overwrite = False
self.should_write_keyframes = True
self.should_write_metadata = True
self.action_name_prefix = ''
self.should_convert_to_samples = False
self.bone_mapping_mode = 'CASE_INSENSITIVE'
self.fps_source = 'SEQUENCE'
self.fps_custom: float = 30.0
self.translation_scale: float = 1.0
self.should_use_config_file = True
self.psa_config: PsaConfig = PsaConfig()
def __init__(self,
action_name_prefix: str = '',
bone_mapping_mode: str = 'CASE_INSENSITIVE',
fps_custom: float = 30.0,
fps_source: str = 'SEQUENCE',
psa_config: PsaConfig = PsaConfig(),
sequence_names: List[str] = None,
should_convert_to_samples: bool = False,
should_overwrite: bool = False,
should_stash: bool = False,
should_use_config_file: bool = True,
should_use_fake_user: bool = False,
should_write_keyframes: bool = True,
should_write_metadata: bool = True,
translation_scale: float = 1.0
):
self.action_name_prefix = action_name_prefix
self.bone_mapping_mode = bone_mapping_mode
self.fps_custom = fps_custom
self.fps_source = fps_source
self.psa_config = psa_config
self.sequence_names = sequence_names if sequence_names is not None else []
self.should_convert_to_samples = should_convert_to_samples
self.should_overwrite = should_overwrite
self.should_stash = should_stash
self.should_use_config_file = should_use_config_file
self.should_use_fake_user = should_use_fake_user
self.should_write_keyframes = should_write_keyframes
self.should_write_metadata = should_write_metadata
self.translation_scale = translation_scale
class ImportBone(object):

View File

@@ -5,7 +5,7 @@ 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 add_psk_export_properties
from .properties import PskExportMixin
from ..builder import build_psk, PskBuildOptions, get_psk_input_objects_for_context, \
get_psk_input_objects_for_collection
from ..writer import write_psk
@@ -14,16 +14,16 @@ from ...shared.ui import draw_bone_filter_mode
def get_materials_for_mesh_objects(depsgraph: Depsgraph, mesh_objects: Iterable[Object]):
materials = []
yielded_materials = set()
for mesh_object in mesh_objects:
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) + ')')
if material not in materials:
materials.append(material)
return materials
if material not in yielded_materials:
yielded_materials.add(material)
yield material
def populate_material_name_list(depsgraph: Depsgraph, mesh_objects, material_list):
@@ -60,7 +60,7 @@ def get_collection_export_operator_from_context(context: Context) -> Optional[ob
class PSK_OT_populate_bone_collection_list(Operator):
bl_idname = 'psk_export.populate_bone_collection_list'
bl_idname = 'psk.export_populate_bone_collection_list'
bl_label = 'Populate Bone Collection List'
bl_description = 'Populate the bone collection list from the armature that will be used in this collection export'
bl_options = {'INTERNAL'}
@@ -79,7 +79,7 @@ class PSK_OT_populate_bone_collection_list(Operator):
class PSK_OT_populate_material_name_list(Operator):
bl_idname = 'psk_export.populate_material_name_list'
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'}
@@ -100,7 +100,7 @@ class PSK_OT_populate_material_name_list(Operator):
class PSK_OT_material_list_move_up(Operator):
bl_idname = 'psk_export.material_list_item_move_up'
bl_idname = 'psk.export_material_list_item_move_up'
bl_label = 'Move Up'
bl_options = {'INTERNAL'}
bl_description = 'Move the selected material up one slot'
@@ -118,7 +118,7 @@ class PSK_OT_material_list_move_up(Operator):
class PSK_OT_material_list_move_down(Operator):
bl_idname = 'psk_export.material_list_item_move_down'
bl_idname = 'psk.export_material_list_item_move_down'
bl_label = 'Move Down'
bl_options = {'INTERNAL'}
bl_description = 'Move the selected material down one slot'
@@ -136,7 +136,7 @@ class PSK_OT_material_list_move_down(Operator):
class PSK_OT_material_list_name_move_up(Operator):
bl_idname = 'psk_export.material_name_list_item_move_up'
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'
@@ -159,7 +159,7 @@ class PSK_OT_material_list_name_move_up(Operator):
class PSK_OT_material_list_name_move_down(Operator):
bl_idname = 'psk_export.material_name_list_item_move_down'
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'
@@ -218,8 +218,8 @@ def get_psk_build_options_from_property_group(mesh_objects: Iterable[Object], p
return options
class PSK_OT_export_collection(Operator, ExportHelper):
bl_idname = 'export.psk_collection'
class PSK_OT_export_collection(Operator, ExportHelper, PskExportMixin):
bl_idname = 'psk.export_collection'
bl_label = 'Export'
bl_options = {'INTERNAL'}
filename_ext = '.psk'
@@ -312,12 +312,8 @@ class PSK_OT_export_collection(Operator, ExportHelper):
add_psk_export_properties(PSK_OT_export_collection)
class PSK_OT_export(Operator, ExportHelper):
bl_idname = 'export.psk'
bl_idname = 'psk.export'
bl_label = 'Export'
bl_options = {'INTERNAL', 'UNDO'}
bl_description = 'Export mesh and armature to PSK'

View File

@@ -60,65 +60,58 @@ def up_axis_update(self, _context):
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 PskExportMixin:
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
)
material_name_list: CollectionProperty(type=PSK_PG_material_name_list_item)
material_name_list_index: IntProperty(default=0)
class PSK_PG_export(PropertyGroup):
class PSK_PG_export(PropertyGroup, PskExportMixin):
pass
add_psk_export_properties(PSK_PG_export)
classes = (
PSK_PG_material_list_item,
PSK_PG_material_name_list_item,

View File

@@ -1,11 +1,11 @@
import os
import sys
from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty
from bpy.props import StringProperty
from bpy.types import Operator, FileHandler, Context
from bpy_extras.io_utils import ImportHelper
from ..importer import PskImportOptions, import_psk
from ..properties import PskImportMixin
from ..reader import read_psk
empty_set = set()
@@ -23,8 +23,8 @@ class PSK_FH_import(FileHandler):
return context.area and context.area.type == 'VIEW_3D'
class PSK_OT_import(Operator, ImportHelper):
bl_idname = 'import_scene.psk'
class PSK_OT_import(Operator, ImportHelper, PskImportMixin):
bl_idname = 'psk.import'
bl_label = 'Import'
bl_options = {'INTERNAL', 'UNDO', 'PRESET'}
bl_description = 'Import a PSK file'
@@ -36,79 +36,6 @@ class PSK_OT_import(Operator, ImportHelper):
maxlen=1024,
default='')
should_import_vertex_colors: BoolProperty(
default=True,
options=empty_set,
name='Import Vertex Colors',
description='Import vertex colors, if available'
)
vertex_color_space: EnumProperty(
name='Vertex Color Space',
options=empty_set,
description='The source vertex color space',
default='SRGBA',
items=(
('LINEAR', 'Linear', ''),
('SRGBA', 'sRGBA', ''),
)
)
should_import_vertex_normals: BoolProperty(
default=True,
name='Import Vertex Normals',
options=empty_set,
description='Import vertex normals, if available'
)
should_import_extra_uvs: BoolProperty(
default=True,
name='Import Extra UVs',
options=empty_set,
description='Import extra UV maps, if available'
)
should_import_mesh: BoolProperty(
default=True,
name='Import Mesh',
options=empty_set,
description='Import mesh'
)
should_import_materials: BoolProperty(
default=True,
name='Import Materials',
options=empty_set,
)
should_import_skeleton: BoolProperty(
default=True,
name='Import Skeleton',
options=empty_set,
description='Import skeleton'
)
bone_length: FloatProperty(
default=1.0,
min=sys.float_info.epsilon,
step=100,
soft_min=1.0,
name='Bone Length',
options=empty_set,
subtype='DISTANCE',
description='Length of the bones'
)
should_import_shape_keys: BoolProperty(
default=True,
name='Import Shape Keys',
options=empty_set,
description='Import shape keys, if available'
)
scale: FloatProperty(
name='Scale',
default=1.0,
soft_min=0.0,
)
bdk_repository_id: StringProperty(
name='BDK Repository ID',
default='',
options=empty_set,
description='The ID of the BDK repository to use for loading materials'
)
def execute(self, context):
psk = read_psk(self.filepath)
@@ -152,7 +79,6 @@ class PSK_OT_import(Operator, ImportHelper):
col.use_property_split = True
col.use_property_decorate = False
col.prop(self, 'scale')
col.prop(self, 'export_space')
mesh_header, mesh_panel = layout.panel('mesh_panel_id', default_closed=False)
mesh_header.prop(self, 'should_import_mesh')

View File

@@ -1,4 +1,6 @@
from bpy.props import EnumProperty
import sys
from bpy.props import EnumProperty, BoolProperty, FloatProperty, StringProperty
from bpy.types import PropertyGroup
mesh_triangle_types_items = (
@@ -42,6 +44,83 @@ def poly_flags_to_triangle_type_and_bit_flags(poly_flags: int) -> (str, set[str]
triangle_bit_flags = {item[0] for item in mesh_triangle_bit_flags_items if item[3] & poly_flags}
return triangle_type, triangle_bit_flags
empty_set = set()
class PskImportMixin:
should_import_vertex_colors: BoolProperty(
default=True,
options=empty_set,
name='Import Vertex Colors',
description='Import vertex colors, if available'
)
vertex_color_space: EnumProperty(
name='Vertex Color Space',
options=empty_set,
description='The source vertex color space',
default='SRGBA',
items=(
('LINEAR', 'Linear', ''),
('SRGBA', 'sRGBA', ''),
)
)
should_import_vertex_normals: BoolProperty(
default=True,
name='Import Vertex Normals',
options=empty_set,
description='Import vertex normals, if available'
)
should_import_extra_uvs: BoolProperty(
default=True,
name='Import Extra UVs',
options=empty_set,
description='Import extra UV maps, if available'
)
should_import_mesh: BoolProperty(
default=True,
name='Import Mesh',
options=empty_set,
description='Import mesh'
)
should_import_materials: BoolProperty(
default=True,
name='Import Materials',
options=empty_set,
)
should_import_skeleton: BoolProperty(
default=True,
name='Import Skeleton',
options=empty_set,
description='Import skeleton'
)
bone_length: FloatProperty(
default=1.0,
min=sys.float_info.epsilon,
step=100,
soft_min=1.0,
name='Bone Length',
options=empty_set,
subtype='DISTANCE',
description='Length of the bones'
)
should_import_shape_keys: BoolProperty(
default=True,
name='Import Shape Keys',
options=empty_set,
description='Import shape keys, if available'
)
scale: FloatProperty(
name='Scale',
default=1.0,
soft_min=0.0,
)
bdk_repository_id: StringProperty(
name='BDK Repository ID',
default='',
options=empty_set,
description='The ID of the BDK repository to use for loading materials'
)
classes = (
PSX_PG_material,