Added bone mapping option to ignore trailing whitespace
Some very old PSKs and PSAs have trailing spaces in the bone names instead of padding the buffer with null bytes. Trailing whitespace will now be ignored by default to maximize compatibility.
This commit is contained in:
@@ -8,7 +8,7 @@ from bpy_extras.io_utils import ImportHelper
|
|||||||
|
|
||||||
from .properties import PsaImportMixin, get_visible_sequences
|
from .properties import PsaImportMixin, get_visible_sequences
|
||||||
from ..config import read_psa_config
|
from ..config import read_psa_config
|
||||||
from ..importer import PsaImportOptions, import_psa
|
from ..importer import BoneMapping, PsaImportOptions, import_psa
|
||||||
from ..reader import PsaReader
|
from ..reader import PsaReader
|
||||||
|
|
||||||
|
|
||||||
@@ -188,7 +188,10 @@ def psa_import_options_from_property_group(pg: PsaImportMixin, sequence_names: I
|
|||||||
options.should_write_metadata = pg.should_write_metadata
|
options.should_write_metadata = pg.should_write_metadata
|
||||||
options.should_write_keyframes = pg.should_write_keyframes
|
options.should_write_keyframes = pg.should_write_keyframes
|
||||||
options.should_convert_to_samples = pg.should_convert_to_samples
|
options.should_convert_to_samples = pg.should_convert_to_samples
|
||||||
options.bone_mapping_mode = pg.bone_mapping_mode
|
options.bone_mapping = BoneMapping(
|
||||||
|
is_case_sensitive=pg.bone_mapping_is_case_sensitive,
|
||||||
|
should_ignore_trailing_whitespace=pg.bone_mapping_should_ignore_trailing_whitespace
|
||||||
|
)
|
||||||
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
|
||||||
@@ -243,7 +246,10 @@ class PSA_OT_import_all(Operator, PsaImportMixin):
|
|||||||
|
|
||||||
options = PsaImportOptions(
|
options = PsaImportOptions(
|
||||||
action_name_prefix=self.action_name_prefix,
|
action_name_prefix=self.action_name_prefix,
|
||||||
bone_mapping_mode=self.bone_mapping_mode,
|
bone_mapping=BoneMapping(
|
||||||
|
is_case_sensitive=self.bone_mapping_is_case_sensitive,
|
||||||
|
should_ignore_trailing_whitespace=self.bone_mapping_should_ignore_trailing_whitespace
|
||||||
|
),
|
||||||
fps_custom=self.fps_custom,
|
fps_custom=self.fps_custom,
|
||||||
fps_source=self.fps_source,
|
fps_source=self.fps_source,
|
||||||
sequence_names=sequence_names,
|
sequence_names=sequence_names,
|
||||||
@@ -380,10 +386,14 @@ class PSA_OT_import(Operator, ImportHelper, PsaImportMixin):
|
|||||||
advanced_header.label(text='Advanced')
|
advanced_header.label(text='Advanced')
|
||||||
|
|
||||||
if advanced_panel:
|
if advanced_panel:
|
||||||
col = advanced_panel.column()
|
bone_mapping_header, bone_mapping_panel = layout.panel('bone_mapping_id', default_closed=False)
|
||||||
|
bone_mapping_header.label(text='Bone Mapping')
|
||||||
|
if bone_mapping_panel:
|
||||||
|
col = bone_mapping_panel.column()
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(self, 'bone_mapping_mode')
|
col.prop(self, 'bone_mapping_is_case_sensitive')
|
||||||
|
col.prop(self, 'bone_mapping_should_ignore_trailing_whitespace')
|
||||||
|
|
||||||
col = advanced_panel.column()
|
col = advanced_panel.column()
|
||||||
col.use_property_split = True
|
col.use_property_split = True
|
||||||
@@ -422,10 +432,15 @@ def draw_psa_import_options_no_panels(layout, pg: PsaImportMixin):
|
|||||||
col.use_property_decorate = False
|
col.use_property_decorate = False
|
||||||
col.prop(pg, 'should_convert_to_samples')
|
col.prop(pg, 'should_convert_to_samples')
|
||||||
|
|
||||||
|
col = layout.column(heading='Bone Mapping')
|
||||||
|
col.use_property_split = True
|
||||||
|
col.use_property_decorate = False
|
||||||
|
col.prop(pg, 'bone_mapping_is_case_sensitive')
|
||||||
|
col.prop(pg, 'bone_mapping_should_ignore_trailing_whitespace')
|
||||||
|
|
||||||
col = layout.column()
|
col = layout.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(pg, 'translation_scale')
|
col.prop(pg, 'translation_scale')
|
||||||
|
|
||||||
col = layout.column(heading='Options')
|
col = layout.column(heading='Options')
|
||||||
|
|||||||
@@ -80,12 +80,13 @@ class PsaImportMixin:
|
|||||||
description='Convert keyframes to read-only samples. '
|
description='Convert keyframes to read-only samples. '
|
||||||
'Recommended if you do not plan on editing the actions directly'
|
'Recommended if you do not plan on editing the actions directly'
|
||||||
)
|
)
|
||||||
bone_mapping_mode: EnumProperty(
|
bone_mapping_is_case_sensitive: BoolProperty(
|
||||||
name='Bone Mapping',
|
default=False,
|
||||||
options=set(),
|
name='Case Sensitive'
|
||||||
description='The method by which bones from the incoming PSA file are mapped to the armature',
|
)
|
||||||
items=bone_mapping_items,
|
bone_mapping_should_ignore_trailing_whitespace: BoolProperty(
|
||||||
default='CASE_INSENSITIVE'
|
default=True,
|
||||||
|
name='Ignore Trailing Whitespace'
|
||||||
)
|
)
|
||||||
fps_source: EnumProperty(name='FPS Source', items=fps_source_items)
|
fps_source: EnumProperty(name='FPS Source', items=fps_source_items)
|
||||||
fps_custom: FloatProperty(
|
fps_custom: FloatProperty(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from typing import Sequence, Iterable, List, Optional, cast as typing_cast
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import re
|
||||||
from bpy.types import Armature, Context, FCurve, Object, Bone, PoseBone
|
from bpy.types import Armature, Context, FCurve, Object, Bone, PoseBone
|
||||||
from mathutils import Vector, Quaternion
|
from mathutils import Vector, Quaternion
|
||||||
|
|
||||||
@@ -9,11 +10,22 @@ from .config import PsaConfig, REMOVE_TRACK_LOCATION, REMOVE_TRACK_ROTATION
|
|||||||
from .reader import PsaReader
|
from .reader import PsaReader
|
||||||
from ..shared.data import PsxBone
|
from ..shared.data import PsxBone
|
||||||
|
|
||||||
|
class BoneMapping:
|
||||||
|
def __init__(self,
|
||||||
|
is_case_sensitive: bool = False,
|
||||||
|
should_ignore_trailing_whitespace: bool = True
|
||||||
|
):
|
||||||
|
self.is_case_sensitive = is_case_sensitive
|
||||||
|
# Ancient PSK and PSA exporters would, for some reason, pad the bone names with spaces
|
||||||
|
# instead of just writing null bytes, probably because the programmers were lazy.
|
||||||
|
# By default, we will ignore trailing whitespace when doing comparisons.
|
||||||
|
self.should_ignore_trailing_whitespace = should_ignore_trailing_whitespace
|
||||||
|
|
||||||
|
|
||||||
class PsaImportOptions(object):
|
class PsaImportOptions(object):
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
action_name_prefix: str = '',
|
action_name_prefix: str = '',
|
||||||
bone_mapping_mode: str = 'CASE_INSENSITIVE',
|
bone_mapping: BoneMapping = BoneMapping(),
|
||||||
fps_custom: float = 30.0,
|
fps_custom: float = 30.0,
|
||||||
fps_source: str = 'SEQUENCE',
|
fps_source: str = 'SEQUENCE',
|
||||||
psa_config: PsaConfig = PsaConfig(),
|
psa_config: PsaConfig = PsaConfig(),
|
||||||
@@ -28,7 +40,7 @@ class PsaImportOptions(object):
|
|||||||
translation_scale: float = 1.0
|
translation_scale: float = 1.0
|
||||||
):
|
):
|
||||||
self.action_name_prefix = action_name_prefix
|
self.action_name_prefix = action_name_prefix
|
||||||
self.bone_mapping_mode = bone_mapping_mode
|
self.bone_mapping = bone_mapping
|
||||||
self.fps_custom = fps_custom
|
self.fps_custom = fps_custom
|
||||||
self.fps_source = fps_source
|
self.fps_source = fps_source
|
||||||
self.psa_config = psa_config
|
self.psa_config = psa_config
|
||||||
@@ -78,20 +90,30 @@ class PsaImportResult:
|
|||||||
self.warnings: List[str] = []
|
self.warnings: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: List[str], bone_mapping_mode: str = 'EXACT') -> Optional[int]:
|
def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: List[str], bone_mapping: BoneMapping) -> Optional[int]:
|
||||||
"""
|
"""
|
||||||
@param psa_bone_name: The name of the PSA bone.
|
@param psa_bone_name: The name of the PSA bone.
|
||||||
@param armature_bone_names: The names of the bones in the armature.
|
@param armature_bone_names: The names of the bones in the armature.
|
||||||
@param bone_mapping_mode: One of `['EXACT', 'CASE_INSENSITIVE']`.
|
@param bone_mapping: Bone mapping information.
|
||||||
@return: The index of the armature bone that corresponds to the given PSA bone, or None if no such bone exists.
|
@return: The index of the armature bone that corresponds to the given PSA bone, or None if no such bone exists.
|
||||||
"""
|
"""
|
||||||
|
# Use regular expressions for bone name matching.
|
||||||
|
pattern = psa_bone_name
|
||||||
|
flags = 0
|
||||||
|
|
||||||
|
if bone_mapping.should_ignore_trailing_whitespace:
|
||||||
|
psa_bone_name = psa_bone_name.rstrip()
|
||||||
|
pattern += r'\s*'
|
||||||
|
|
||||||
|
if not bone_mapping.is_case_sensitive:
|
||||||
|
flags = re.IGNORECASE
|
||||||
|
|
||||||
|
pattern = re.compile(pattern, flags)
|
||||||
|
|
||||||
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names):
|
for armature_bone_index, armature_bone_name in enumerate(armature_bone_names):
|
||||||
if bone_mapping_mode == 'CASE_INSENSITIVE':
|
if re.fullmatch(pattern, armature_bone_name):
|
||||||
if armature_bone_name.lower() == psa_bone_name.lower():
|
|
||||||
return armature_bone_index
|
|
||||||
else:
|
|
||||||
if armature_bone_name == psa_bone_name:
|
|
||||||
return armature_bone_index
|
return armature_bone_index
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -161,7 +183,7 @@ def import_psa(context: Context, psa_reader: PsaReader, armature_object: Object,
|
|||||||
|
|
||||||
for psa_bone_index, psa_bone in enumerate(psa_reader.bones):
|
for psa_bone_index, psa_bone in enumerate(psa_reader.bones):
|
||||||
psa_bone_name: str = psa_bone.name.decode('windows-1252')
|
psa_bone_name: str = psa_bone.name.decode('windows-1252')
|
||||||
armature_bone_index = _get_armature_bone_index_for_psa_bone(psa_bone_name, armature_bone_names, options.bone_mapping_mode)
|
armature_bone_index = _get_armature_bone_index_for_psa_bone(psa_bone_name, armature_bone_names, options.bone_mapping)
|
||||||
if armature_bone_index is not None:
|
if armature_bone_index is not None:
|
||||||
# Ensure that no other PSA bone has been mapped to this armature bone yet.
|
# Ensure that no other PSA bone has been mapped to this armature bone yet.
|
||||||
if armature_bone_index not in armature_to_psa_bone_indices:
|
if armature_bone_index not in armature_to_psa_bone_indices:
|
||||||
|
|||||||
Reference in New Issue
Block a user