From b20d19d072236da3975a21ef456c0b8b7c4e8e40 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Fri, 11 Aug 2023 02:09:16 -0700 Subject: [PATCH 01/10] Added NLA track sequence source option (reimplementation of `feature-nla-track-sequence-source` branch). This just needs a bit of time in the oven to test it out. --- io_scene_psk_psa/helpers.py | 2 +- io_scene_psk_psa/psa/export/operators.py | 85 +++++++++++++---------- io_scene_psk_psa/psa/export/properties.py | 73 +++++++++++++++++-- io_scene_psk_psa/psa/export/ui.py | 2 +- 4 files changed, 118 insertions(+), 44 deletions(-) diff --git a/io_scene_psk_psa/helpers.py b/io_scene_psk_psa/helpers.py index fe683e0..d7b7980 100644 --- a/io_scene_psk_psa/helpers.py +++ b/io_scene_psk_psa/helpers.py @@ -34,7 +34,7 @@ def rgb_to_srgb(c: float): return 12.92 * c -def get_nla_strips_in_timeframe(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]: +def get_nla_strips_in_frame_range(animation_data: AnimData, frame_min: float, frame_max: float) -> List[NlaStrip]: if animation_data is None: return [] strips = [] diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index 407d144..a6f1a04 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -8,10 +8,10 @@ from bpy.types import Context, Armature, Action, Object, AnimData, TimelineMarke from bpy_extras.io_utils import ExportHelper from bpy_types import Operator -from io_scene_psk_psa.helpers import populate_bone_group_list, get_nla_strips_in_timeframe -from io_scene_psk_psa.psa.builder import build_psa, PsaBuildSequence, PsaBuildOptions -from io_scene_psk_psa.psa.export.properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences -from io_scene_psk_psa.psa.writer import write_psa +from .properties import PSA_PG_export, PSA_PG_export_action_list_item, filter_sequences +from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions +from ..writer import write_psa +from ...helpers import populate_bone_group_list, get_nla_strips_in_frame_range def is_action_for_armature(armature: Armature, action: Action): @@ -150,7 +150,7 @@ def get_timeline_marker_sequence_frame_ranges(animation_data: AnimData, context: if next_marker_index < len(sorted_timeline_markers): # There is a next marker. Use that next marker's frame position as the last frame of this sequence. frame_end = sorted_timeline_markers[next_marker_index].frame - nla_strips = get_nla_strips_in_timeframe(animation_data, marker.frame, frame_end) + nla_strips = get_nla_strips_in_frame_range(animation_data, marker.frame, frame_end) if len(nla_strips) > 0: frame_end = min(frame_end, max(map(lambda nla_strip: nla_strip.frame_end, nla_strips))) frame_start = max(frame_start, min(map(lambda nla_strip: nla_strip.frame_start, nla_strips))) @@ -254,12 +254,18 @@ class PSA_OT_export(Operator, ExportHelper): # SOURCE layout.prop(pg, 'sequence_source', text='Source') - if pg.sequence_source == 'TIMELINE_MARKERS': + if pg.sequence_source in {'TIMELINE_MARKERS', 'NLA_TRACK_STRIPS'}: # ANIMDATA SOURCE layout.prop(pg, 'should_override_animation_data') if pg.should_override_animation_data: layout.prop(pg, 'animation_data_override', text='') + if pg.sequence_source == 'NLA_TRACK_STRIPS': + flow = layout.grid_flow() + flow.use_property_split = True + flow.use_property_decorate = False + flow.prop(pg, 'nla_track') + # SELECT ALL/NONE row = layout.row(align=True) row.label(text='Select') @@ -269,25 +275,19 @@ class PSA_OT_export(Operator, ExportHelper): # ACTIONS if pg.sequence_source == 'ACTIONS': rows = max(3, min(len(pg.action_list), 10)) - layout.template_list('PSA_UL_export_sequences', '', pg, 'action_list', pg, 'action_list_index', rows=rows) - - col = layout.column() - col.use_property_split = True - col.use_property_decorate = False - col.prop(pg, 'sequence_name_prefix') - col.prop(pg, 'sequence_name_suffix') - elif pg.sequence_source == 'TIMELINE_MARKERS': rows = max(3, min(len(pg.marker_list), 10)) - layout.template_list('PSA_UL_export_sequences', '', pg, 'marker_list', pg, 'marker_list_index', - rows=rows) + layout.template_list('PSA_UL_export_sequences', '', pg, 'marker_list', pg, 'marker_list_index', rows=rows) + elif pg.sequence_source == 'NLA_TRACK_STRIPS': + rows = max(3, min(len(pg.nla_strip_list), 10)) + layout.template_list('PSA_UL_export_sequences', '', pg, 'nla_strip_list', pg, 'nla_strip_list_index', rows=rows) - col = layout.column() - col.use_property_split = True - col.use_property_decorate = False - col.prop(pg, 'sequence_name_prefix') - col.prop(pg, 'sequence_name_suffix') + col = layout.column() + col.use_property_split = True + col.use_property_decorate = False + col.prop(pg, 'sequence_name_prefix') + col.prop(pg, 'sequence_name_suffix') # Determine if there is going to be a naming conflict and display an error, if so. selected_items = [x for x in pg.action_list if x.is_selected] @@ -360,6 +360,8 @@ class PSA_OT_export(Operator, ExportHelper): raise RuntimeError('No actions were selected for export') elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_names) == 0: raise RuntimeError('No timeline markers were selected for export') + elif pg.sequence_source == 'NLA_TRACK_STRIPS' and len(pg.nla_strip_list) == 0: + raise RuntimeError('No NLA track strips were selected for export') # Populate the export sequence list. animation_data_object = get_animation_data_object(context) @@ -371,29 +373,38 @@ class PSA_OT_export(Operator, ExportHelper): export_sequences: List[PsaBuildSequence] = [] if pg.sequence_source == 'ACTIONS': - for action in filter(lambda x: x.is_selected, pg.action_list): - if len(action.action.fcurves) == 0: + for action_item in filter(lambda x: x.is_selected, pg.action_list): + if len(action_item.action.fcurves) == 0: continue export_sequence = PsaBuildSequence() - export_sequence.nla_state.action = action.action - export_sequence.name = action.name - export_sequence.nla_state.frame_start = action.frame_start - export_sequence.nla_state.frame_end = action.frame_end - export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action.action]) - export_sequence.compression_ratio = action.action.psa_export.compression_ratio - export_sequence.key_quota = action.action.psa_export.key_quota + export_sequence.nla_state.action = action_item.action + export_sequence.name = action_item.name + export_sequence.nla_state.frame_start = action_item.frame_start + export_sequence.nla_state.frame_end = action_item.frame_end + export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [action_item.action]) + export_sequence.compression_ratio = action_item.action.psa_export.compression_ratio + export_sequence.key_quota = action_item.action.psa_export.key_quota export_sequences.append(export_sequence) elif pg.sequence_source == 'TIMELINE_MARKERS': - for marker in pg.marker_list: + for marker_item in filter(lambda x: x.is_selected, pg.marker_list): export_sequence = PsaBuildSequence() - export_sequence.name = marker.name + export_sequence.name = marker_item.name export_sequence.nla_state.action = None - export_sequence.nla_state.frame_start = marker.frame_start - export_sequence.nla_state.frame_end = marker.frame_end + export_sequence.nla_state.frame_start = marker_item.frame_start + export_sequence.nla_state.frame_end = marker_item.frame_end nla_strips_actions = set( - map(lambda x: x.action, get_nla_strips_in_timeframe(animation_data, marker.frame_start, marker.frame_end))) + map(lambda x: x.action, get_nla_strips_in_frame_range(animation_data, marker_item.frame_start, marker_item.frame_end))) export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, nla_strips_actions) export_sequences.append(export_sequence) + elif pg.sequence_source == 'NLA_TRACK_STRIPS': + for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list): + export_sequence = PsaBuildSequence() + export_sequence.name = nla_strip_item.name + export_sequence.nla_state.action = nla_strip_item.action + export_sequence.nla_state.frame_start = nla_strip_item.frame_start + export_sequence.nla_state.frame_end = nla_strip_item.frame_end + export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [nla_strip_item.action]) + export_sequences.append(export_sequence) else: raise ValueError(f'Unhandled sequence source: {pg.sequence_source}') @@ -432,6 +443,8 @@ class PSA_OT_export_actions_select_all(Operator): return pg.action_list elif pg.sequence_source == 'TIMELINE_MARKERS': return pg.marker_list + elif pg.sequence_source == 'NLA_TRACK_STRIPS': + return pg.nla_strip_list return None @classmethod @@ -463,6 +476,8 @@ class PSA_OT_export_actions_deselect_all(Operator): return pg.action_list elif pg.sequence_source == 'TIMELINE_MARKERS': return pg.marker_list + elif pg.sequence_source == 'NLA_TRACK_STRIPS': + return pg.nla_strip_list return None @classmethod diff --git a/io_scene_psk_psa/psa/export/properties.py b/io_scene_psk_psa/psa/export/properties.py index 1f75f74..5a87548 100644 --- a/io_scene_psk_psa/psa/export/properties.py +++ b/io_scene_psk_psa/psa/export/properties.py @@ -1,10 +1,11 @@ +import re import sys from fnmatch import fnmatch -from typing import List +from typing import List, Optional from bpy.props import BoolProperty, PointerProperty, EnumProperty, FloatProperty, CollectionProperty, IntProperty, \ StringProperty -from bpy.types import PropertyGroup, Object, Action +from bpy.types import PropertyGroup, Object, Action, AnimData, Context from ...types import PSX_PG_bone_group_list_item @@ -25,7 +26,7 @@ class PSA_PG_export_action_list_item(PropertyGroup): is_pose_marker: BoolProperty(options={'HIDDEN'}) -class PSA_PG_export_timeline_markers(PropertyGroup): +class PSA_PG_export_timeline_markers(PropertyGroup): # TODO: rename this to singular marker_index: IntProperty() name: StringProperty() is_selected: BoolProperty(default=True) @@ -33,6 +34,51 @@ class PSA_PG_export_timeline_markers(PropertyGroup): frame_end: IntProperty(options={'HIDDEN'}) +class PSA_PG_export_nla_strip_list_item(PropertyGroup): + name: StringProperty() + action: PointerProperty(type=Action) + frame_start: FloatProperty() + frame_end: FloatProperty() + is_selected: BoolProperty(default=True) + + +def nla_track_update_cb(self: 'PSA_PG_export', context: Context) -> None: + self.nla_strip_list.clear() + if context.object is None or context.object.animation_data is None: + return + match = re.match(r'^(\d+).+$', self.nla_track) + self.nla_track_index = int(match.group(1)) if match else -1 + if self.nla_track_index >= 0: + nla_track = context.object.animation_data.nla_tracks[self.nla_track_index] + for nla_strip in nla_track.strips: + strip: PSA_PG_export_nla_strip_list_item = self.nla_strip_list.add() + strip.action = nla_strip.action + strip.name = nla_strip.name + strip.frame_start = nla_strip.frame_start + strip.frame_end = nla_strip.frame_end + + +def get_animation_data(pg: 'PSA_PG_export', context: Context) -> Optional[AnimData]: + animation_data_object = context.object + if pg.should_override_animation_data: + animation_data_object = pg.animation_data_override + return animation_data_object.animation_data if animation_data_object else None + + +def nla_track_search_cb(self, context: Context, edit_text: str): + pg = getattr(context.scene, 'psa_export') + animation_data = get_animation_data(pg, context) + if animation_data is None: + return + for index, nla_track in enumerate(animation_data.nla_tracks): + yield f'{index} - {nla_track.name}' + + +def animation_data_override_update_cb(self: 'PSA_PG_export', context: Context): + # Reset NLA track selection + self.nla_track = '' + + class PSA_PG_export(PropertyGroup): root_motion: BoolProperty( name='Root Motion', @@ -46,10 +92,12 @@ class PSA_PG_export(PropertyGroup): name='Override Animation Data', options=empty_set, default=False, - description='Use the animation data from a different object instead of the selected object' + description='Use the animation data from a different object instead of the selected object', + update=animation_data_override_update_cb, ) animation_data_override: PointerProperty( type=Object, + update=animation_data_override_update_cb, poll=psa_export_property_group_animation_data_override_poll ) sequence_source: EnumProperty( @@ -58,10 +106,18 @@ class PSA_PG_export(PropertyGroup): description='', items=( ('ACTIONS', 'Actions', 'Sequences will be exported using actions', 'ACTION', 0), - ('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences will be exported using timeline markers', 'MARKER_HLT', - 1), + ('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences are delineated by scene timeline markers', 'MARKER_HLT', 1), + ('NLA_TRACK_STRIPS', 'NLA Track Strips', 'Sequences are delineated by the start & end times of strips on the selected NLA track', 'NLA', 2) ) ) + nla_track: StringProperty( + name='NLA Track', + options=empty_set, + description='', + search=nla_track_search_cb, + update=nla_track_update_cb + ) + nla_track_index: IntProperty(name='NLA Track Index', default=-1) fps_source: EnumProperty( name='FPS Source', options=empty_set, @@ -80,6 +136,8 @@ class PSA_PG_export(PropertyGroup): action_list_index: IntProperty(default=0) marker_list: CollectionProperty(type=PSA_PG_export_timeline_markers) marker_list_index: IntProperty(default=0) + nla_strip_list: CollectionProperty(type=PSA_PG_export_nla_strip_list_item) + nla_strip_list_index: IntProperty(default=0) bone_filter_mode: EnumProperty( name='Bone Filter', options=empty_set, @@ -145,7 +203,7 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]: if not pg.sequence_filter_asset: for i, sequence in enumerate(sequences): - if hasattr(sequence, 'action') and sequence.action.asset_data is not None: + if hasattr(sequence, 'action') and sequence.action is not None and sequence.action.asset_data is not None: flt_flags[i] &= ~bitflag_filter_item if not pg.sequence_filter_pose_marker: @@ -164,5 +222,6 @@ def filter_sequences(pg: PSA_PG_export, sequences) -> List[int]: classes = ( PSA_PG_export_action_list_item, PSA_PG_export_timeline_markers, + PSA_PG_export_nla_strip_list_item, PSA_PG_export, ) diff --git a/io_scene_psk_psa/psa/export/ui.py b/io_scene_psk_psa/psa/export/ui.py index ad9d29a..d302265 100644 --- a/io_scene_psk_psa/psa/export/ui.py +++ b/io_scene_psk_psa/psa/export/ui.py @@ -16,7 +16,7 @@ class PSA_UL_export_sequences(UIList): item = cast(PSA_PG_export_action_list_item, item) is_pose_marker = hasattr(item, 'is_pose_marker') and item.is_pose_marker layout.prop(item, 'is_selected', icon_only=True, text=item.name) - if hasattr(item, 'action') and item.action.asset_data is not None: + if hasattr(item, 'action') and item.action is not None and item.action.asset_data is not None: layout.label(text='', icon='ASSET_MANAGER') row = layout.row(align=True) From b6e5a13e5f0cf7d087a04c41fb3a526c08775421 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Tue, 15 Aug 2023 15:16:07 -0700 Subject: [PATCH 02/10] Fuxed a bug where it was not possible to export from markers Stupid typo, thanks Python --- io_scene_psk_psa/psa/export/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index a6f1a04..ec0adf5 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -358,7 +358,7 @@ class PSA_OT_export(Operator, ExportHelper): # Ensure that we actually have items that we are going to be exporting. if pg.sequence_source == 'ACTIONS' and len(pg.action_list) == 0: raise RuntimeError('No actions were selected for export') - elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_names) == 0: + elif pg.sequence_source == 'TIMELINE_MARKERS' and len(pg.marker_list) == 0: raise RuntimeError('No timeline markers were selected for export') elif pg.sequence_source == 'NLA_TRACK_STRIPS' and len(pg.nla_strip_list) == 0: raise RuntimeError('No NLA track strips were selected for export') From c4c00ca49e676e4b3fd3688a5c2cc248363953cb Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Tue, 15 Aug 2023 15:26:44 -0700 Subject: [PATCH 03/10] Fixed a bug where exporting animations from NLA strip markers would result in corrupted animations --- io_scene_psk_psa/psa/export/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index ec0adf5..48fcd37 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -400,7 +400,7 @@ class PSA_OT_export(Operator, ExportHelper): for nla_strip_item in filter(lambda x: x.is_selected, pg.nla_strip_list): export_sequence = PsaBuildSequence() export_sequence.name = nla_strip_item.name - export_sequence.nla_state.action = nla_strip_item.action + export_sequence.nla_state.action = None export_sequence.nla_state.frame_start = nla_strip_item.frame_start export_sequence.nla_state.frame_end = nla_strip_item.frame_end export_sequence.fps = get_sequence_fps(context, pg.fps_source, pg.fps_custom, [nla_strip_item.action]) From 07ccc8c650a5fd3ef336f32b18fdee253a374209 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Wed, 16 Aug 2023 12:00:26 -0700 Subject: [PATCH 04/10] Added sequence reversing functionality for timeline markers --- io_scene_psk_psa/psa/export/operators.py | 39 +++++++++++------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index 48fcd37..682641d 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -80,12 +80,15 @@ def update_actions_and_timeline_markers(context: Context, armature: Armature): continue if marker_name.startswith('#'): continue - item = pg.marker_list.add() - item.name = marker_name - item.is_selected = False frame_start, frame_end = sequence_frame_ranges[marker_name] - item.frame_start = frame_start - item.frame_end = frame_end + sequences = get_sequences_from_name_and_frame_range(marker_name, frame_start, frame_end) + for (sequence_name, frame_start, frame_end) in sequences: + item = pg.marker_list.add() + item.name = sequence_name + item.is_selected = False + frame_start, frame_end = sequence_frame_ranges[marker_name] + item.frame_start = frame_start + item.frame_end = frame_end def get_sequence_fps(context: Context, fps_source: str, fps_custom: float, actions: Iterable[Action]) -> float: @@ -174,11 +177,9 @@ 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]]: - frame_start = int(action.frame_range[0]) - frame_end = int(action.frame_range[1]) +def get_sequences_from_name_and_frame_range(name: str, frame_start: int, frame_end: int) -> List[Tuple[str, int, int]]: reversed_pattern = r'(.+)/(.+)' - reversed_match = re.match(reversed_pattern, action.name) + reversed_match = re.match(reversed_pattern, name) if reversed_match: forward_name = reversed_match.group(1) backwards_name = reversed_match.group(2) @@ -187,7 +188,13 @@ def get_sequences_from_action(action: Action) -> List[Tuple[str, int, int]]: (backwards_name, frame_end, frame_start) ] else: - return [(action.name, frame_start, frame_end)] + return [(name, frame_start, frame_end)] + + +def get_sequences_from_action(action: Action) -> List[Tuple[str, int, int]]: + 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) def get_sequences_from_action_pose_marker(action: Action, pose_markers: List[TimelineMarker], pose_marker: TimelineMarker, pose_marker_index: int) -> List[Tuple[str, int, int]]: @@ -196,17 +203,7 @@ def get_sequences_from_action_pose_marker(action: Action, pose_markers: List[Tim frame_end = pose_markers[pose_marker_index + 1].frame else: frame_end = int(action.frame_range[1]) - reversed_pattern = r'(.+)/(.+)' - reversed_match = re.match(reversed_pattern, pose_marker.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) - ] - else: - return [(pose_marker.name, frame_start, frame_end)] + return get_sequences_from_name_and_frame_range(pose_marker.name, frame_start, frame_end) def get_visible_sequences(pg: PSA_PG_export, sequences) -> List[PSA_PG_export_action_list_item]: From 60c7f2125a2f0ef5e0183447f0992c2ef90c8b9c Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Wed, 16 Aug 2023 15:52:28 -0700 Subject: [PATCH 05/10] Fixed a bug with the previous commit. --- io_scene_psk_psa/psa/export/operators.py | 1 - 1 file changed, 1 deletion(-) diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index 682641d..2176834 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -86,7 +86,6 @@ def update_actions_and_timeline_markers(context: Context, armature: Armature): item = pg.marker_list.add() item.name = sequence_name item.is_selected = False - frame_start, frame_end = sequence_frame_ranges[marker_name] item.frame_start = frame_start item.frame_end = frame_end From 8c74987f5b0d185cc3152b2cb9bb02a3e18d0cfa Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Fri, 18 Aug 2023 18:20:29 -0700 Subject: [PATCH 06/10] Added human readable errors when bone, material or sequence names cannot be encoded into the Windows-1252 code page --- io_scene_psk_psa/psa/builder.py | 11 +++++++++-- io_scene_psk_psa/psk/builder.py | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/io_scene_psk_psa/psa/builder.py b/io_scene_psk_psa/psa/builder.py index 2a23ee9..c8b831e 100644 --- a/io_scene_psk_psa/psa/builder.py +++ b/io_scene_psk_psa/psa/builder.py @@ -91,7 +91,11 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa: # Build list of PSA bones. for bone in bones: psa_bone = Psa.Bone() - psa_bone.name = bytes(bone.name, encoding='windows-1252') + + try: + psa_bone.name = bytes(bone.name, encoding='windows-1252') + except UnicodeEncodeError: + raise RuntimeError(f'Bone name "{bone.name}" contains characters that cannot be encoded in the Windows-1252 codepage') try: parent_index = bones.index(bone.parent) @@ -165,7 +169,10 @@ def build_psa(context: bpy.types.Context, options: PsaBuildOptions) -> Psa: frame_step = -frame_step psa_sequence = Psa.Sequence() - psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252') + try: + psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252') + except UnicodeEncodeError: + raise RuntimeError(f'Sequence name "{export_sequence.name}" contains characters that cannot be encoded in the Windows-1252 codepage') psa_sequence.frame_count = frame_count psa_sequence.frame_start_index = frame_start_index psa_sequence.fps = frame_count / sequence_duration diff --git a/io_scene_psk_psa/psk/builder.py b/io_scene_psk_psa/psk/builder.py index 99a3784..7a22124 100644 --- a/io_scene_psk_psa/psk/builder.py +++ b/io_scene_psk_psa/psk/builder.py @@ -88,7 +88,11 @@ def build_psk(context, options: PskBuildOptions) -> Psk: for bone in bones: psk_bone = Psk.Bone() - psk_bone.name = bytes(bone.name, encoding='windows-1252') + try: + psk_bone.name = bytes(bone.name, encoding='windows-1252') + except UnicodeEncodeError: + raise RuntimeError( + f'Bone name "{bone.name}" contains characters that cannot be encoded in the Windows-1252 codepage') psk_bone.flags = 0 psk_bone.children_count = 0 @@ -129,7 +133,10 @@ def build_psk(context, options: PskBuildOptions) -> Psk: for material_name in material_names: psk_material = Psk.Material() - psk_material.name = bytes(material_name, encoding='windows-1252') + try: + psk_material.name = bytes(material_name, encoding='windows-1252') + except UnicodeEncodeError: + raise RuntimeError(f'Material name "{material_name}" contains characters that cannot be encoded in the Windows-1252 codepage') psk_material.texture_index = len(psk.materials) psk.materials.append(psk_material) From 560ec8fecd8954e412a616de2284e2a913b8158f Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Fri, 18 Aug 2023 18:22:16 -0700 Subject: [PATCH 07/10] Fixed a bug where exported PSKs would always use the mesh data's material instead of the object's material --- io_scene_psk_psa/psk/builder.py | 2 +- io_scene_psk_psa/psk/export/operators.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/io_scene_psk_psa/psk/builder.py b/io_scene_psk_psa/psk/builder.py index 7a22124..1053672 100644 --- a/io_scene_psk_psa/psk/builder.py +++ b/io_scene_psk_psa/psk/builder.py @@ -143,7 +143,7 @@ def build_psk(context, options: PskBuildOptions) -> Psk: for input_mesh_object in input_objects.mesh_objects: # MATERIALS - material_indices = [material_names.index(material.name) for material in input_mesh_object.data.materials] + material_indices = [material_names.index(material_slot.material.name) for material_slot in input_mesh_object.material_slots] # MESH DATA if options.use_raw_mesh_data: diff --git a/io_scene_psk_psa/psk/export/operators.py b/io_scene_psk_psa/psk/export/operators.py index cb48caa..ee799a0 100644 --- a/io_scene_psk_psa/psk/export/operators.py +++ b/io_scene_psk_psa/psk/export/operators.py @@ -22,10 +22,11 @@ def populate_material_list(mesh_objects, material_list): material_names = [] for mesh_object in mesh_objects: - for i, material in enumerate(mesh_object.data.materials): + for i, material_slot in enumerate(mesh_object.material_slots): + material = material_slot.material # TODO: put this in the poll arg? if material is None: - raise RuntimeError('Material cannot be empty (index ' + str(i) + ')') + raise RuntimeError('Material slot cannot be empty (index ' + str(i) + ')') if material.name not in material_names: material_names.append(material.name) From 3de1f075ddbc193f78a0a640908eeb443cf22cf1 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Fri, 25 Aug 2023 16:56:59 -0700 Subject: [PATCH 08/10] Minor clean-up --- io_scene_psk_psa/helpers.py | 21 +-------------------- io_scene_psk_psa/psk/importer.py | 2 +- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/io_scene_psk_psa/helpers.py b/io_scene_psk_psa/helpers.py index d7b7980..9740d3a 100644 --- a/io_scene_psk_psa/helpers.py +++ b/io_scene_psk_psa/helpers.py @@ -1,4 +1,3 @@ -import datetime import re import typing from collections import Counter @@ -9,24 +8,6 @@ import bpy.types from bpy.types import NlaStrip, Object, AnimData -class Timer: - def __enter__(self): - self.start = datetime.datetime.now() - self.interval = None - return self - - def __exit__(self, *args): - self.end = datetime.datetime.now() - self.interval = self.end - self.start - - @property - def duration(self): - if self.interval is not None: - return self.interval - else: - return datetime.datetime.now() - self.start - - def rgb_to_srgb(c: float): if c > 0.0031308: return 1.055 * (pow(c, (1.0 / 2.4))) - 0.055 @@ -143,7 +124,7 @@ def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_g # Split out the bone indices and the instigator bone names into separate lists. # We use the bone names for the return values because the bone name is a more universal way of referencing them. - # For example, users of this function may modify bone lists, which would invalidate the indices and require a + # For example, users of this function may modify bone lists, which would invalidate the indices and require an # index mapping scheme to resolve it. Using strings is more comfy and results in less code downstream. instigator_bone_names = [bones[x[1]].name if x[1] is not None else None for x in bone_indices] bone_names = [bones[x[0]].name for x in bone_indices] diff --git a/io_scene_psk_psa/psk/importer.py b/io_scene_psk_psa/psk/importer.py index 88d518b..0696edd 100644 --- a/io_scene_psk_psa/psk/importer.py +++ b/io_scene_psk_psa/psk/importer.py @@ -131,7 +131,7 @@ def import_psk(psk: Psk, context, options: PskImportOptions) -> PskImportResult: # Material already exists, just re-use it. material = bpy.data.materials[material_name] elif is_bdk_addon_loaded() and psk.has_material_references: - # Material does not yet exist and we have the BDK addon installed. + # Material does not yet exist, and we have the BDK addon installed. # Attempt to load it using BDK addon's operator. material_reference = psk.material_references[material_index] if material_reference and bpy.ops.bdk.link_material(reference=material_reference) == {'FINISHED'}: From ea5d0c6ac2d74bbe65a255f3466013981cb74b5e Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Sat, 26 Aug 2023 12:18:45 -0700 Subject: [PATCH 09/10] Incremented version to 5.1.0 --- io_scene_psk_psa/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/io_scene_psk_psa/__init__.py b/io_scene_psk_psa/__init__.py index 9442648..708bc25 100644 --- a/io_scene_psk_psa/__init__.py +++ b/io_scene_psk_psa/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "PSK/PSA Importer/Exporter", "author": "Colin Basnett, Yurii Ti", - "version": (5, 0, 4), + "version": (5, 1, 0), "blender": (3, 4, 0), "description": "PSK/PSA Import/Export (.psk/.psa)", "warning": "", From 214f19ff8cdb29f8d1f6191446ea2e9312744933 Mon Sep 17 00:00:00 2001 From: Colin Basnett Date: Tue, 5 Sep 2023 23:35:39 -0700 Subject: [PATCH 10/10] Fix for #47 This code had been refactored but not tested with the no-armature workflow --- io_scene_psk_psa/psk/builder.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/io_scene_psk_psa/psk/builder.py b/io_scene_psk_psa/psk/builder.py index 1053672..9ac80f3 100644 --- a/io_scene_psk_psa/psk/builder.py +++ b/io_scene_psk_psa/psk/builder.py @@ -154,8 +154,10 @@ def build_psk(context, options: PskBuildOptions) -> Psk: # Temporarily force the armature into the rest position. # We will undo this later. - old_pose_position = armature_object.data.pose_position - armature_object.data.pose_position = 'REST' + old_pose_position = None + if armature_object is not None: + old_pose_position = armature_object.data.pose_position + armature_object.data.pose_position = 'REST' depsgraph = context.evaluated_depsgraph_get() bm = bmesh.new() @@ -171,7 +173,8 @@ def build_psk(context, options: PskBuildOptions) -> Psk: mesh_object.vertex_groups.new(name=vertex_group.name) # Restore the previous pose position on the armature. - armature_object.data.pose_position = old_pose_position + if old_pose_position is not None: + armature_object.data.pose_position = old_pose_position vertex_offset = len(psk.points)