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:
@@ -1,9 +1,9 @@
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import List, Iterable, Dict, Tuple, cast, Optional
|
from typing import List, Iterable, Dict, Tuple, Optional
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.props import StringProperty
|
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_extras.io_utils import ExportHelper
|
||||||
from bpy_types import Operator
|
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}')
|
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:
|
if len(action.fcurves) == 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if obj.animation_data is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if obj.type != 'ARMATURE':
|
||||||
|
return False
|
||||||
|
|
||||||
version = SemanticVersion(bpy.app.version)
|
version = SemanticVersion(bpy.app.version)
|
||||||
|
|
||||||
if version < SemanticVersion((4, 4, 0)):
|
if version < SemanticVersion((4, 4, 0)):
|
||||||
import re
|
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:
|
for fcurve in action.fcurves:
|
||||||
match = re.match(r'pose\.bones\[\"([^\"]+)\"](\[\"([^\"]+)\"])?', fcurve.data_path)
|
match = re.match(r'pose\.bones\[\"([^\"]+)\"](\[\"([^\"]+)\"])?', fcurve.data_path)
|
||||||
if not match:
|
if not match:
|
||||||
@@ -46,11 +53,8 @@ def is_action_for_armature(armature: Armature, action: Action):
|
|||||||
if bone_name in bone_names:
|
if bone_name in bone_names:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
# Look up the armature by ID and check if its data block pointer matches the armature.
|
# In 4.4.0 and later, we can check if the object's action slot handle matches an action slot handle in the action.
|
||||||
for slot in filter(lambda x: x.id_root == 'OBJECT', action.slots):
|
if any(obj.animation_data.action_slot_handle == slot.handle for slot in 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
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -71,15 +75,13 @@ def update_actions_and_timeline_markers(context: Context):
|
|||||||
if animation_data is None:
|
if animation_data is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
active_armature = cast(Armature, context.active_object.data)
|
|
||||||
|
|
||||||
# Populate actions list.
|
# Populate actions list.
|
||||||
for action in bpy.data.actions:
|
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
|
continue
|
||||||
|
|
||||||
if action.name != '' and not action.name.startswith('#'):
|
|
||||||
for (name, frame_start, frame_end) in get_sequences_from_action(action):
|
for (name, frame_start, frame_end) in get_sequences_from_action(action):
|
||||||
|
print(name)
|
||||||
item = pg.action_list.add()
|
item = pg.action_list.add()
|
||||||
item.action = action
|
item.action = action
|
||||||
item.name = name
|
item.name = name
|
||||||
@@ -217,13 +219,15 @@ def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context:
|
|||||||
return sequence_frame_ranges
|
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_start = int(action.frame_range[0])
|
||||||
frame_end = int(action.frame_range[1])
|
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
|
frame_start = pose_marker.frame
|
||||||
sequence_name = pose_marker.name
|
sequence_name = pose_marker.name
|
||||||
if pose_marker.name.startswith('!'):
|
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
|
frame_end = pose_markers[pose_marker_index + 1].frame
|
||||||
else:
|
else:
|
||||||
frame_end = int(action.frame_range[1])
|
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]:
|
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):
|
class PSA_OT_export(Operator, ExportHelper):
|
||||||
bl_idname = 'psa_export.operator'
|
bl_idname = 'psa.export'
|
||||||
bl_label = 'Export'
|
bl_label = 'Export'
|
||||||
bl_options = {'INTERNAL', 'UNDO'}
|
bl_options = {'INTERNAL', 'UNDO'}
|
||||||
bl_description = 'Export actions to PSA'
|
bl_description = 'Export actions to PSA'
|
||||||
@@ -515,7 +519,7 @@ class PSA_OT_export(Operator, ExportHelper):
|
|||||||
|
|
||||||
|
|
||||||
class PSA_OT_export_actions_select_all(Operator):
|
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_label = 'Select All'
|
||||||
bl_description = 'Select all visible sequences'
|
bl_description = 'Select all visible sequences'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
@@ -552,7 +556,7 @@ class PSA_OT_export_actions_select_all(Operator):
|
|||||||
|
|
||||||
|
|
||||||
class PSA_OT_export_actions_deselect_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_label = 'Deselect All'
|
||||||
bl_description = 'Deselect all visible sequences'
|
bl_description = 'Deselect all visible sequences'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
@@ -587,7 +591,7 @@ class PSA_OT_export_actions_deselect_all(Operator):
|
|||||||
|
|
||||||
|
|
||||||
class PSA_OT_export_bone_collections_select_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_label = 'Select All'
|
||||||
bl_description = 'Select all bone collections'
|
bl_description = 'Select all bone collections'
|
||||||
bl_options = {'INTERNAL'}
|
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):
|
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_label = 'Deselect All'
|
||||||
bl_description = 'Deselect all bone collections'
|
bl_description = 'Deselect all bone collections'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from fnmatch import fnmatch
|
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, \
|
from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty, \
|
||||||
StringProperty
|
StringProperty
|
||||||
@@ -52,18 +52,16 @@ class PSA_PG_export_nla_strip_list_item(PropertyGroup):
|
|||||||
is_selected: BoolProperty(default=True)
|
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_pattern = r'(.+)/(.+)'
|
||||||
reversed_match = re.match(reversed_pattern, name)
|
reversed_match = re.match(reversed_pattern, name)
|
||||||
if reversed_match:
|
if reversed_match:
|
||||||
forward_name = reversed_match.group(1)
|
forward_name = reversed_match.group(1)
|
||||||
backwards_name = reversed_match.group(2)
|
backwards_name = reversed_match.group(2)
|
||||||
return [
|
yield forward_name, frame_start, frame_end
|
||||||
(forward_name, frame_start, frame_end),
|
yield backwards_name, frame_end, frame_start
|
||||||
(backwards_name, frame_end, frame_start)
|
|
||||||
]
|
|
||||||
else:
|
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:
|
def nla_track_update_cb(self: 'PSA_PG_export', context: Context) -> None:
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import Iterable
|
||||||
|
|
||||||
from bpy.props import StringProperty, CollectionProperty
|
from bpy.props import StringProperty, CollectionProperty
|
||||||
from bpy.types import Operator, Event, Context, FileHandler, OperatorFileListElement, Object
|
from bpy.types import Operator, Event, Context, FileHandler, OperatorFileListElement, Object
|
||||||
from bpy_extras.io_utils import ImportHelper
|
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 ..config import read_psa_config
|
||||||
from ..importer import import_psa, PsaImportOptions
|
from ..importer import import_psa, PsaImportOptions
|
||||||
from ..reader import PsaReader
|
from ..reader import PsaReader
|
||||||
|
|
||||||
|
|
||||||
class PSA_OT_import_sequences_from_text(Operator):
|
def psa_import_poll(cls, context: Context):
|
||||||
bl_idname = 'psa_import.sequences_select_from_text'
|
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_label = 'Select By Text List'
|
||||||
bl_description = 'Select sequences by name from text list'
|
bl_description = 'Select sequences by name from text list'
|
||||||
bl_options = {'INTERNAL', 'UNDO'}
|
bl_options = {'INTERNAL', 'UNDO'}
|
||||||
@@ -49,7 +57,7 @@ class PSA_OT_import_sequences_from_text(Operator):
|
|||||||
|
|
||||||
|
|
||||||
class PSA_OT_import_sequences_select_all(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_label = 'All'
|
||||||
bl_description = 'Select all sequences'
|
bl_description = 'Select all sequences'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
@@ -70,7 +78,7 @@ class PSA_OT_import_sequences_select_all(Operator):
|
|||||||
|
|
||||||
|
|
||||||
class PSA_OT_import_sequences_deselect_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_label = 'None'
|
||||||
bl_description = 'Deselect all visible sequences'
|
bl_description = 'Deselect all visible sequences'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
@@ -113,8 +121,8 @@ def on_psa_file_path_updated(cls, context):
|
|||||||
load_psa_file(context, cls.filepath)
|
load_psa_file(context, cls.filepath)
|
||||||
|
|
||||||
|
|
||||||
class PSA_OT_import_multiple(Operator):
|
class PSA_OT_import_drag_and_drop(Operator, PsaImportMixin):
|
||||||
bl_idname = 'psa_import.import_multiple'
|
bl_idname = 'psa.import_drag_and_drop'
|
||||||
bl_label = 'Import PSA'
|
bl_label = 'Import PSA'
|
||||||
bl_description = 'Import multiple PSA files'
|
bl_description = 'Import multiple PSA files'
|
||||||
bl_options = {'INTERNAL', 'UNDO'}
|
bl_options = {'INTERNAL', 'UNDO'}
|
||||||
@@ -122,23 +130,25 @@ class PSA_OT_import_multiple(Operator):
|
|||||||
directory: StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE', 'HIDDEN'})
|
directory: StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE', 'HIDDEN'})
|
||||||
files: CollectionProperty(type=OperatorFileListElement, options={'SKIP_SAVE', 'HIDDEN'})
|
files: CollectionProperty(type=OperatorFileListElement, options={'SKIP_SAVE', 'HIDDEN'})
|
||||||
|
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
pg = getattr(context.scene, 'psa_import')
|
|
||||||
warnings = []
|
warnings = []
|
||||||
|
sequence_names = []
|
||||||
|
|
||||||
for file in self.files:
|
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)
|
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)
|
sequence_names.extend(file_sequence_names)
|
||||||
result.warnings.extend(warnings)
|
|
||||||
|
|
||||||
if len(result.warnings) > 0:
|
result = _import_psa(context, options, psa_path, context.view_layer.objects.active)
|
||||||
message = f'Imported {len(sequence_names)} action(s) with {len(result.warnings)} warning(s)\n'
|
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)
|
self.report({'INFO'}, message)
|
||||||
for warning in result.warnings:
|
for warning in warnings:
|
||||||
self.report({'WARNING'}, warning)
|
self.report({'WARNING'}, warning)
|
||||||
|
|
||||||
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
|
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
|
||||||
@@ -146,7 +156,7 @@ class PSA_OT_import_multiple(Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
def invoke(self, context: Context, event):
|
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
|
active_object = context.view_layer.objects.active
|
||||||
if active_object is None or active_object.type != 'ARMATURE':
|
if active_object is None or active_object.type != 'ARMATURE':
|
||||||
self.report({'ERROR_INVALID_CONTEXT'}, 'The active object must be an 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):
|
def draw(self, context):
|
||||||
layout = self.layout
|
layout = self.layout
|
||||||
pg = getattr(context.scene, 'psa_import')
|
draw_psa_import_options_no_panels(layout, self)
|
||||||
draw_psa_import_options_no_panels(layout, pg)
|
|
||||||
|
|
||||||
|
|
||||||
def _import_psa(context,
|
def psa_import_options_from_property_group(pg: PsaImportMixin, sequence_names: Iterable[str]) -> PsaImportOptions:
|
||||||
pg,
|
|
||||||
filepath: str,
|
|
||||||
sequence_names: List[str],
|
|
||||||
armature_object: Object
|
|
||||||
):
|
|
||||||
options = 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_use_fake_user = pg.should_use_fake_user
|
||||||
options.should_stash = pg.should_stash
|
options.should_stash = pg.should_stash
|
||||||
options.action_name_prefix = pg.action_name_prefix if pg.should_use_action_name_prefix else ''
|
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_source = pg.fps_source
|
||||||
options.fps_custom = pg.fps_custom
|
options.fps_custom = pg.fps_custom
|
||||||
options.translation_scale = pg.translation_scale
|
options.translation_scale = pg.translation_scale
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def _import_psa(context,
|
||||||
|
options: PsaImportOptions,
|
||||||
|
filepath: str,
|
||||||
|
armature_object: Object
|
||||||
|
):
|
||||||
warnings = []
|
warnings = []
|
||||||
|
|
||||||
if options.should_use_config_file:
|
if options.should_use_config_file:
|
||||||
@@ -189,7 +200,7 @@ def _import_psa(context,
|
|||||||
config_path = Path(filepath).with_suffix('.config')
|
config_path = Path(filepath).with_suffix('.config')
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
warnings.append(f'Failed to read PSA config file: {e}')
|
warnings.append(f'Failed to read PSA config file: {e}')
|
||||||
|
|
||||||
@@ -201,8 +212,8 @@ def _import_psa(context,
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class PSA_OT_import(Operator, ImportHelper):
|
class PSA_OT_import(Operator, ImportHelper, PsaImportMixin):
|
||||||
bl_idname = 'psa_import.import'
|
bl_idname = 'psa.import'
|
||||||
bl_label = 'Import'
|
bl_label = 'Import'
|
||||||
bl_description = 'Import the selected animations into the scene as actions'
|
bl_description = 'Import the selected animations into the scene as actions'
|
||||||
bl_options = {'INTERNAL', 'UNDO'}
|
bl_options = {'INTERNAL', 'UNDO'}
|
||||||
@@ -218,29 +229,25 @@ class PSA_OT_import(Operator, ImportHelper):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
active_object = context.view_layer.objects.active
|
return psa_import_poll(cls, context)
|
||||||
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
|
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
pg = getattr(context.scene, 'psa_import')
|
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')
|
self.report({'ERROR_INVALID_CONTEXT'}, 'No sequences selected')
|
||||||
return {'CANCELLED'}
|
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:
|
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)
|
self.report({'WARNING'}, message)
|
||||||
for warning in result.warnings:
|
for warning in result.warnings:
|
||||||
self.report({'WARNING'}, warning)
|
self.report({'WARNING'}, warning)
|
||||||
else:
|
else:
|
||||||
self.report({'INFO'}, f'Imported {len(sequence_names)} action(s)')
|
self.report({'INFO'}, f'Imported {len(options.sequence_names)} action(s)')
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
@@ -271,7 +278,7 @@ class PSA_OT_import(Operator, ImportHelper):
|
|||||||
|
|
||||||
row2 = col.row(align=True)
|
row2 = col.row(align=True)
|
||||||
row2.label(text='Select')
|
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_select_all.bl_idname, text='All', icon='CHECKBOX_HLT')
|
||||||
row2.operator(PSA_OT_import_sequences_deselect_all.bl_idname, text='None', icon='CHECKBOX_DEHLT')
|
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 = sequences_panel.column(heading='')
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(pg, 'fps_source')
|
col.prop(self, 'fps_source')
|
||||||
if pg.fps_source == 'CUSTOM':
|
if self.fps_source == 'CUSTOM':
|
||||||
col.prop(pg, 'fps_custom')
|
col.prop(self, 'fps_custom')
|
||||||
col.prop(pg, 'should_overwrite')
|
col.prop(self, 'should_overwrite')
|
||||||
col.prop(pg, 'should_use_action_name_prefix')
|
col.prop(self, 'should_use_action_name_prefix')
|
||||||
if pg.should_use_action_name_prefix:
|
if self.should_use_action_name_prefix:
|
||||||
col.prop(pg, 'action_name_prefix')
|
col.prop(self, 'action_name_prefix')
|
||||||
|
|
||||||
data_header, data_panel = layout.panel('data_panel_id', default_closed=False)
|
data_header, data_panel = layout.panel('data_panel_id', default_closed=False)
|
||||||
data_header.label(text='Data')
|
data_header.label(text='Data')
|
||||||
@@ -296,14 +303,14 @@ class PSA_OT_import(Operator, ImportHelper):
|
|||||||
col = data_panel.column(heading='Write')
|
col = data_panel.column(heading='Write')
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(pg, 'should_write_keyframes')
|
col.prop(self, 'should_write_keyframes')
|
||||||
col.prop(pg, 'should_write_metadata')
|
col.prop(self, 'should_write_metadata')
|
||||||
|
|
||||||
if pg.should_write_keyframes:
|
if self.should_write_keyframes:
|
||||||
col = col.column(heading='Keyframes')
|
col = col.column(heading='Keyframes')
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
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, advanced_panel = layout.panel('advanced_panel_id', default_closed=True)
|
||||||
advanced_header.label(text='Advanced')
|
advanced_header.label(text='Advanced')
|
||||||
@@ -312,22 +319,22 @@ class PSA_OT_import(Operator, ImportHelper):
|
|||||||
col = advanced_panel.column()
|
col = advanced_panel.column()
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(pg, 'bone_mapping_mode')
|
col.prop(self, 'bone_mapping_mode')
|
||||||
|
|
||||||
col = advanced_panel.column()
|
col = advanced_panel.column()
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
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 = advanced_panel.column(heading='Options')
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(pg, 'should_use_fake_user')
|
col.prop(self, 'should_use_fake_user')
|
||||||
col.prop(pg, 'should_stash')
|
col.prop(self, 'should_stash')
|
||||||
col.prop(pg, 'should_use_config_file')
|
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 = layout.column(heading='Sequences')
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
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')
|
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_idname = 'PSA_FH_import'
|
||||||
bl_label = 'File handler for Unreal PSA import'
|
bl_label = 'File handler for Unreal PSA import'
|
||||||
bl_import_operator = 'psa_import.import_multiple'
|
bl_import_operator = PSA_OT_import_drag_and_drop.bl_idname
|
||||||
bl_export_operator = 'psa_export.export'
|
# bl_export_operator = 'psa_export.export'
|
||||||
bl_file_extensions = '.psa'
|
bl_file_extensions = '.psa'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -380,8 +387,8 @@ class PSA_FH_import(FileHandler):
|
|||||||
classes = (
|
classes = (
|
||||||
PSA_OT_import_sequences_select_all,
|
PSA_OT_import_sequences_select_all,
|
||||||
PSA_OT_import_sequences_deselect_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,
|
||||||
PSA_OT_import_multiple,
|
PSA_OT_import_drag_and_drop,
|
||||||
PSA_FH_import,
|
PSA_FH_import,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,11 +23,23 @@ class PSA_PG_data(PropertyGroup):
|
|||||||
sequence_count: IntProperty(default=0)
|
sequence_count: IntProperty(default=0)
|
||||||
|
|
||||||
|
|
||||||
class PSA_PG_import(PropertyGroup):
|
bone_mapping_items = (
|
||||||
psa_error: StringProperty(default='')
|
('EXACT', 'Exact', 'Bone names must match exactly.', 'EXACT', 0),
|
||||||
psa: PointerProperty(type=PSA_PG_data)
|
('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),
|
||||||
sequence_list: CollectionProperty(type=PSA_PG_import_action_list_item)
|
)
|
||||||
sequence_list_index: IntProperty(name='', default=0)
|
|
||||||
|
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',
|
should_use_fake_user: BoolProperty(default=True, name='Fake User',
|
||||||
description='Assign each imported action a fake user so that the data block is '
|
description='Assign each imported action a fake user so that the data block is '
|
||||||
'saved even it has no users',
|
'saved even it has no users',
|
||||||
@@ -56,7 +68,7 @@ class PSA_PG_import(PropertyGroup):
|
|||||||
sequence_use_filter_invert: BoolProperty(default=False, options=empty_set)
|
sequence_use_filter_invert: BoolProperty(default=False, options=empty_set)
|
||||||
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression',
|
sequence_use_filter_regex: BoolProperty(default=False, name='Regular Expression',
|
||||||
description='Filter using regular expressions', options=empty_set)
|
description='Filter using regular expressions', options=empty_set)
|
||||||
select_text: PointerProperty(type=Text)
|
|
||||||
should_convert_to_samples: BoolProperty(
|
should_convert_to_samples: BoolProperty(
|
||||||
default=False,
|
default=False,
|
||||||
name='Convert to Samples',
|
name='Convert to Samples',
|
||||||
@@ -67,18 +79,10 @@ class PSA_PG_import(PropertyGroup):
|
|||||||
name='Bone Mapping',
|
name='Bone Mapping',
|
||||||
options=empty_set,
|
options=empty_set,
|
||||||
description='The method by which bones from the incoming PSA file are mapped to the armature',
|
description='The method by which bones from the incoming PSA file are mapped to the armature',
|
||||||
items=(
|
items=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),
|
|
||||||
),
|
|
||||||
default='CASE_INSENSITIVE'
|
default='CASE_INSENSITIVE'
|
||||||
)
|
)
|
||||||
fps_source: EnumProperty(name='FPS Source', items=(
|
fps_source: EnumProperty(name='FPS Source', items=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_custom: FloatProperty(
|
fps_custom: FloatProperty(
|
||||||
default=30.0,
|
default=30.0,
|
||||||
name='Custom FPS',
|
name='Custom FPS',
|
||||||
@@ -89,10 +93,7 @@ class PSA_PG_import(PropertyGroup):
|
|||||||
soft_max=60.0,
|
soft_max=60.0,
|
||||||
step=100,
|
step=100,
|
||||||
)
|
)
|
||||||
compression_ratio_source: EnumProperty(name='Compression Ratio Source', items=(
|
compression_ratio_source: EnumProperty(name='Compression Ratio Source', items=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_custom: FloatProperty(
|
compression_ratio_custom: FloatProperty(
|
||||||
default=1.0,
|
default=1.0,
|
||||||
name='Custom Compression Ratio',
|
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]:
|
def filter_sequences(pg: PSA_PG_import, sequences) -> List[int]:
|
||||||
bitflag_filter_item = 1 << 30
|
bitflag_filter_item = 1 << 30
|
||||||
flt_flags = [bitflag_filter_item] * len(sequences)
|
flt_flags = [bitflag_filter_item] * len(sequences)
|
||||||
|
|||||||
@@ -12,21 +12,36 @@ from .reader import PsaReader
|
|||||||
|
|
||||||
|
|
||||||
class PsaImportOptions(object):
|
class PsaImportOptions(object):
|
||||||
def __init__(self):
|
def __init__(self,
|
||||||
self.should_use_fake_user = False
|
action_name_prefix: str = '',
|
||||||
self.should_stash = False
|
bone_mapping_mode: str = 'CASE_INSENSITIVE',
|
||||||
self.sequence_names = []
|
fps_custom: float = 30.0,
|
||||||
self.should_overwrite = False
|
fps_source: str = 'SEQUENCE',
|
||||||
self.should_write_keyframes = True
|
psa_config: PsaConfig = PsaConfig(),
|
||||||
self.should_write_metadata = True
|
sequence_names: List[str] = None,
|
||||||
self.action_name_prefix = ''
|
should_convert_to_samples: bool = False,
|
||||||
self.should_convert_to_samples = False
|
should_overwrite: bool = False,
|
||||||
self.bone_mapping_mode = 'CASE_INSENSITIVE'
|
should_stash: bool = False,
|
||||||
self.fps_source = 'SEQUENCE'
|
should_use_config_file: bool = True,
|
||||||
self.fps_custom: float = 30.0
|
should_use_fake_user: bool = False,
|
||||||
self.translation_scale: float = 1.0
|
should_write_keyframes: bool = True,
|
||||||
self.should_use_config_file = True
|
should_write_metadata: bool = True,
|
||||||
self.psa_config: PsaConfig = PsaConfig()
|
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):
|
class ImportBone(object):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from bpy.props import StringProperty
|
|||||||
from bpy.types import Operator, Context, Object, Collection, SpaceProperties, Depsgraph, Material
|
from bpy.types import Operator, Context, Object, Collection, SpaceProperties, Depsgraph, Material
|
||||||
from bpy_extras.io_utils import ExportHelper
|
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, \
|
from ..builder import build_psk, PskBuildOptions, get_psk_input_objects_for_context, \
|
||||||
get_psk_input_objects_for_collection
|
get_psk_input_objects_for_collection
|
||||||
from ..writer import write_psk
|
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]):
|
def get_materials_for_mesh_objects(depsgraph: Depsgraph, mesh_objects: Iterable[Object]):
|
||||||
materials = []
|
yielded_materials = set()
|
||||||
for mesh_object in mesh_objects:
|
for mesh_object in mesh_objects:
|
||||||
evaluated_mesh_object = mesh_object.evaluated_get(depsgraph)
|
evaluated_mesh_object = mesh_object.evaluated_get(depsgraph)
|
||||||
for i, material_slot in enumerate(evaluated_mesh_object.material_slots):
|
for i, material_slot in enumerate(evaluated_mesh_object.material_slots):
|
||||||
material = material_slot.material
|
material = material_slot.material
|
||||||
if material is None:
|
if material is None:
|
||||||
raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')')
|
raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')')
|
||||||
if material not in materials:
|
if material not in yielded_materials:
|
||||||
materials.append(material)
|
yielded_materials.add(material)
|
||||||
return materials
|
yield material
|
||||||
|
|
||||||
|
|
||||||
def populate_material_name_list(depsgraph: Depsgraph, mesh_objects, material_list):
|
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):
|
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_label = 'Populate Bone Collection List'
|
||||||
bl_description = 'Populate the bone collection list from the armature that will be used in this collection export'
|
bl_description = 'Populate the bone collection list from the armature that will be used in this collection export'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
@@ -79,7 +79,7 @@ class PSK_OT_populate_bone_collection_list(Operator):
|
|||||||
|
|
||||||
|
|
||||||
class PSK_OT_populate_material_name_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_label = 'Populate Material Name List'
|
||||||
bl_description = 'Populate the material name list from the objects that will be used in this export'
|
bl_description = 'Populate the material name list from the objects that will be used in this export'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
@@ -100,7 +100,7 @@ class PSK_OT_populate_material_name_list(Operator):
|
|||||||
|
|
||||||
|
|
||||||
class PSK_OT_material_list_move_up(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_label = 'Move Up'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
bl_description = 'Move the selected material up one slot'
|
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):
|
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_label = 'Move Down'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
bl_description = 'Move the selected material down one slot'
|
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):
|
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_label = 'Move Up'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
bl_description = 'Move the selected material name up one slot'
|
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):
|
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_label = 'Move Down'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
bl_description = 'Move the selected material name down one slot'
|
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
|
return options
|
||||||
|
|
||||||
|
|
||||||
class PSK_OT_export_collection(Operator, ExportHelper):
|
class PSK_OT_export_collection(Operator, ExportHelper, PskExportMixin):
|
||||||
bl_idname = 'export.psk_collection'
|
bl_idname = 'psk.export_collection'
|
||||||
bl_label = 'Export'
|
bl_label = 'Export'
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
filename_ext = '.psk'
|
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):
|
class PSK_OT_export(Operator, ExportHelper):
|
||||||
bl_idname = 'export.psk'
|
bl_idname = 'psk.export'
|
||||||
bl_label = 'Export'
|
bl_label = 'Export'
|
||||||
bl_options = {'INTERNAL', 'UNDO'}
|
bl_options = {'INTERNAL', 'UNDO'}
|
||||||
bl_description = 'Export mesh and armature to PSK'
|
bl_description = 'Export mesh and armature to PSK'
|
||||||
|
|||||||
@@ -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')
|
self.forward_axis = next((axis for axis in axis_identifiers if axis != self.up_axis), 'X')
|
||||||
|
|
||||||
|
|
||||||
|
class PskExportMixin:
|
||||||
# In order to share the same properties between the PSA and PSK export properties, we need to define the properties in a
|
object_eval_state: EnumProperty(
|
||||||
# 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,
|
items=object_eval_state_items,
|
||||||
name='Object Evaluation State',
|
name='Object Evaluation State',
|
||||||
default='EVALUATED'
|
default='EVALUATED'
|
||||||
)
|
)
|
||||||
cls.__annotations__['should_exclude_hidden_meshes'] = BoolProperty(
|
should_exclude_hidden_meshes: BoolProperty(
|
||||||
default=False,
|
default=False,
|
||||||
name='Visible Only',
|
name='Visible Only',
|
||||||
description='Export only visible meshes'
|
description='Export only visible meshes'
|
||||||
)
|
)
|
||||||
cls.__annotations__['scale'] = FloatProperty(
|
scale: FloatProperty(
|
||||||
name='Scale',
|
name='Scale',
|
||||||
default=1.0,
|
default=1.0,
|
||||||
description='Scale factor to apply to the exported mesh and armature',
|
description='Scale factor to apply to the exported mesh and armature',
|
||||||
min=0.0001,
|
min=0.0001,
|
||||||
soft_max=100.0
|
soft_max=100.0
|
||||||
)
|
)
|
||||||
cls.__annotations__['export_space'] = EnumProperty(
|
export_space: EnumProperty(
|
||||||
name='Export Space',
|
name='Export Space',
|
||||||
description='Space to export the mesh in',
|
description='Space to export the mesh in',
|
||||||
items=export_space_items,
|
items=export_space_items,
|
||||||
default='WORLD'
|
default='WORLD'
|
||||||
)
|
)
|
||||||
cls.__annotations__['bone_filter_mode'] = EnumProperty(
|
bone_filter_mode: EnumProperty(
|
||||||
name='Bone Filter',
|
name='Bone Filter',
|
||||||
options=empty_set,
|
options=empty_set,
|
||||||
description='',
|
description='',
|
||||||
items=bone_filter_mode_items,
|
items=bone_filter_mode_items,
|
||||||
)
|
)
|
||||||
cls.__annotations__['bone_collection_list'] = CollectionProperty(type=PSX_PG_bone_collection_list_item)
|
bone_collection_list: CollectionProperty(type=PSX_PG_bone_collection_list_item)
|
||||||
cls.__annotations__['bone_collection_list_index'] = IntProperty(default=0)
|
bone_collection_list_index: IntProperty(default=0)
|
||||||
cls.__annotations__['forward_axis'] = EnumProperty(
|
forward_axis: EnumProperty(
|
||||||
name='Forward',
|
name='Forward',
|
||||||
items=forward_items,
|
items=forward_items,
|
||||||
default='X',
|
default='X',
|
||||||
update=forward_axis_update
|
update=forward_axis_update
|
||||||
)
|
)
|
||||||
cls.__annotations__['up_axis'] = EnumProperty(
|
up_axis: EnumProperty(
|
||||||
name='Up',
|
name='Up',
|
||||||
items=up_items,
|
items=up_items,
|
||||||
default='Z',
|
default='Z',
|
||||||
update=up_axis_update
|
update=up_axis_update
|
||||||
)
|
)
|
||||||
cls.__annotations__['material_name_list'] = CollectionProperty(type=PSK_PG_material_name_list_item)
|
material_name_list: CollectionProperty(type=PSK_PG_material_name_list_item)
|
||||||
cls.__annotations__['material_name_list_index'] = IntProperty(default=0)
|
material_name_list_index: IntProperty(default=0)
|
||||||
|
|
||||||
|
|
||||||
class PSK_PG_export(PropertyGroup):
|
class PSK_PG_export(PropertyGroup, PskExportMixin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
add_psk_export_properties(PSK_PG_export)
|
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
PSK_PG_material_list_item,
|
PSK_PG_material_list_item,
|
||||||
PSK_PG_material_name_list_item,
|
PSK_PG_material_name_list_item,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import os
|
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.types import Operator, FileHandler, Context
|
||||||
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
|
||||||
|
from ..properties import PskImportMixin
|
||||||
from ..reader import read_psk
|
from ..reader import read_psk
|
||||||
|
|
||||||
empty_set = set()
|
empty_set = set()
|
||||||
@@ -23,8 +23,8 @@ class PSK_FH_import(FileHandler):
|
|||||||
return context.area and context.area.type == 'VIEW_3D'
|
return context.area and context.area.type == 'VIEW_3D'
|
||||||
|
|
||||||
|
|
||||||
class PSK_OT_import(Operator, ImportHelper):
|
class PSK_OT_import(Operator, ImportHelper, PskImportMixin):
|
||||||
bl_idname = 'import_scene.psk'
|
bl_idname = 'psk.import'
|
||||||
bl_label = 'Import'
|
bl_label = 'Import'
|
||||||
bl_options = {'INTERNAL', 'UNDO', 'PRESET'}
|
bl_options = {'INTERNAL', 'UNDO', 'PRESET'}
|
||||||
bl_description = 'Import a PSK file'
|
bl_description = 'Import a PSK file'
|
||||||
@@ -36,79 +36,6 @@ class PSK_OT_import(Operator, ImportHelper):
|
|||||||
maxlen=1024,
|
maxlen=1024,
|
||||||
default='')
|
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):
|
def execute(self, context):
|
||||||
psk = read_psk(self.filepath)
|
psk = read_psk(self.filepath)
|
||||||
|
|
||||||
@@ -152,7 +79,6 @@ class PSK_OT_import(Operator, ImportHelper):
|
|||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(self, 'scale')
|
col.prop(self, 'scale')
|
||||||
col.prop(self, 'export_space')
|
|
||||||
|
|
||||||
mesh_header, mesh_panel = layout.panel('mesh_panel_id', default_closed=False)
|
mesh_header, mesh_panel = layout.panel('mesh_panel_id', default_closed=False)
|
||||||
mesh_header.prop(self, 'should_import_mesh')
|
mesh_header.prop(self, 'should_import_mesh')
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from bpy.props import EnumProperty
|
import sys
|
||||||
|
|
||||||
|
from bpy.props import EnumProperty, BoolProperty, FloatProperty, StringProperty
|
||||||
from bpy.types import PropertyGroup
|
from bpy.types import PropertyGroup
|
||||||
|
|
||||||
mesh_triangle_types_items = (
|
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}
|
triangle_bit_flags = {item[0] for item in mesh_triangle_bit_flags_items if item[3] & poly_flags}
|
||||||
return triangle_type, triangle_bit_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 = (
|
classes = (
|
||||||
PSX_PG_material,
|
PSX_PG_material,
|
||||||
|
|||||||
Reference in New Issue
Block a user