Added the ability to export sequences using timeline markers (WIP, not thoroughly tested yet!)
A bunch of clean up
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
|
from bpy.types import NlaStrip
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
|
||||||
def rgb_to_srgb(c):
|
def rgb_to_srgb(c):
|
||||||
@@ -8,19 +10,45 @@ def rgb_to_srgb(c):
|
|||||||
return 12.92 * c
|
return 12.92 * c
|
||||||
|
|
||||||
|
|
||||||
|
def get_nla_strips_ending_at_frame(object, frame) -> List[NlaStrip]:
|
||||||
|
if object is None or object.animation_data is None:
|
||||||
|
return []
|
||||||
|
strips = []
|
||||||
|
for nla_track in object.animation_data.nla_tracks:
|
||||||
|
for strip in nla_track.strips:
|
||||||
|
if strip.frame_end == frame:
|
||||||
|
strips.append(strip)
|
||||||
|
return strips
|
||||||
|
|
||||||
|
|
||||||
|
def get_nla_strips_in_timeframe(object, frame_min, frame_max) -> List[NlaStrip]:
|
||||||
|
if object is None or object.animation_data is None:
|
||||||
|
return []
|
||||||
|
strips = []
|
||||||
|
for nla_track in object.animation_data.nla_tracks:
|
||||||
|
for strip in nla_track.strips:
|
||||||
|
if strip.frame_end >= frame_min and strip.frame_start <= frame_max:
|
||||||
|
strips.append(strip)
|
||||||
|
return strips
|
||||||
|
|
||||||
|
|
||||||
def populate_bone_group_list(armature_object, bone_group_list):
|
def populate_bone_group_list(armature_object, bone_group_list):
|
||||||
bone_group_list.clear()
|
bone_group_list.clear()
|
||||||
|
|
||||||
|
if armature_object and armature_object.pose:
|
||||||
|
bone_group_counts = Counter(map(lambda x: x.bone_group, armature_object.pose.bones))
|
||||||
|
|
||||||
item = bone_group_list.add()
|
item = bone_group_list.add()
|
||||||
item.name = '(unassigned)'
|
item.name = 'Unassigned'
|
||||||
item.index = -1
|
item.index = -1
|
||||||
|
item.count = 0 if None not in bone_group_counts else bone_group_counts[None]
|
||||||
item.is_selected = True
|
item.is_selected = True
|
||||||
|
|
||||||
if armature_object and armature_object.pose:
|
|
||||||
for bone_group_index, bone_group in enumerate(armature_object.pose.bone_groups):
|
for bone_group_index, bone_group in enumerate(armature_object.pose.bone_groups):
|
||||||
item = bone_group_list.add()
|
item = bone_group_list.add()
|
||||||
item.name = bone_group.name
|
item.name = bone_group.name
|
||||||
item.index = bone_group_index
|
item.index = bone_group_index
|
||||||
|
item.count = 0 if bone_group not in bone_group_counts else bone_group_counts[bone_group]
|
||||||
item.is_selected = True
|
item.is_selected = True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
from .data import *
|
from .data import *
|
||||||
from ..helpers import *
|
from ..helpers import *
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
class PsaBuilderOptions(object):
|
class PsaBuilderOptions(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.sequence_source = 'ACTIONS'
|
||||||
self.actions = []
|
self.actions = []
|
||||||
|
self.marker_names = []
|
||||||
self.bone_filter_mode = 'ALL'
|
self.bone_filter_mode = 'ALL'
|
||||||
self.bone_group_indices = []
|
self.bone_group_indices = []
|
||||||
self.should_use_original_sequence_names = False
|
self.should_use_original_sequence_names = False
|
||||||
|
self.should_trim_timeline_marker_sequences = True
|
||||||
|
|
||||||
|
|
||||||
class PsaBuilder(object):
|
class PsaBuilder(object):
|
||||||
@@ -25,6 +29,12 @@ class PsaBuilder(object):
|
|||||||
if armature.animation_data is None:
|
if armature.animation_data is None:
|
||||||
raise RuntimeError('No animation data for armature')
|
raise RuntimeError('No animation data for armature')
|
||||||
|
|
||||||
|
# Ensure that we actually have items that we are going to be exporting.
|
||||||
|
if options.sequence_source == 'ACTIONS' and len(options.actions) == 0:
|
||||||
|
raise RuntimeError('No actions were selected for export')
|
||||||
|
elif options.sequence_source == 'TIMELINE_MARKERS' and len(options.marker_names) == 0:
|
||||||
|
raise RuntimeError('No timeline markers were selected for export')
|
||||||
|
|
||||||
psa = Psa()
|
psa = Psa()
|
||||||
|
|
||||||
bones = list(armature.data.bones)
|
bones = list(armature.data.bones)
|
||||||
@@ -59,6 +69,7 @@ class PsaBuilder(object):
|
|||||||
raise RuntimeError('Exported bone hierarchy must have a single root bone.'
|
raise RuntimeError('Exported bone hierarchy must have a single root bone.'
|
||||||
f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}')
|
f'The bone hierarchy marked for export has {len(root_bones)} root bones: {root_bone_names}')
|
||||||
|
|
||||||
|
# Build list of PSA bones.
|
||||||
for pose_bone in bones:
|
for pose_bone in bones:
|
||||||
psa_bone = Psa.Bone()
|
psa_bone = Psa.Bone()
|
||||||
psa_bone.name = bytes(pose_bone.name, encoding='utf-8')
|
psa_bone.name = bytes(pose_bone.name, encoding='utf-8')
|
||||||
@@ -95,28 +106,65 @@ class PsaBuilder(object):
|
|||||||
|
|
||||||
psa.bones.append(psa_bone)
|
psa.bones.append(psa_bone)
|
||||||
|
|
||||||
frame_start_index = 0
|
# Populate the export sequence list.
|
||||||
|
class ExportSequence:
|
||||||
|
def __init__(self):
|
||||||
|
self.name = ''
|
||||||
|
self.frame_min = 0
|
||||||
|
self.frame_max = 0
|
||||||
|
self.action = None
|
||||||
|
self.nla_strips_to_be_muted = []
|
||||||
|
|
||||||
|
export_sequences = []
|
||||||
|
|
||||||
|
if options.sequence_source == 'ACTIONS':
|
||||||
for action in options.actions:
|
for action in options.actions:
|
||||||
if len(action.fcurves) == 0:
|
if len(action.fcurves) == 0:
|
||||||
continue
|
continue
|
||||||
|
export_sequence = ExportSequence()
|
||||||
|
export_sequence.action = action
|
||||||
|
export_sequence.name = get_psa_sequence_name(action, options.should_use_original_sequence_names)
|
||||||
|
export_sequence.frame_min, export_sequence.frame_max = [int(x) for x in action.frame_range]
|
||||||
|
export_sequences.append(export_sequence)
|
||||||
|
pass
|
||||||
|
elif options.sequence_source == 'TIMELINE_MARKERS':
|
||||||
|
sequence_frame_ranges = self.get_timeline_marker_sequence_frame_ranges(armature, context, options)
|
||||||
|
for name, (frame_min, frame_max) in sequence_frame_ranges.items():
|
||||||
|
export_sequence = ExportSequence()
|
||||||
|
export_sequence.action = None
|
||||||
|
export_sequence.name = name
|
||||||
|
export_sequence.frame_min = frame_min
|
||||||
|
export_sequence.frame_max = frame_max
|
||||||
|
export_sequence.nla_strips_to_be_muted = get_nla_strips_ending_at_frame(armature, frame_min)
|
||||||
|
export_sequences.append(export_sequence)
|
||||||
|
else:
|
||||||
|
raise ValueError(f'Unhandled sequence source: {options.sequence_source}')
|
||||||
|
|
||||||
armature.animation_data.action = action
|
frame_start_index = 0
|
||||||
|
|
||||||
|
# Now build the PSA sequences.
|
||||||
|
# We actually alter the timeline frame and simply record the resultant pose bone matrices.
|
||||||
|
for export_sequence in export_sequences:
|
||||||
|
armature.animation_data.action = export_sequence.action
|
||||||
context.view_layer.update()
|
context.view_layer.update()
|
||||||
|
|
||||||
frame_min, frame_max = [int(x) for x in action.frame_range]
|
psa_sequence = Psa.Sequence()
|
||||||
|
|
||||||
sequence = Psa.Sequence()
|
frame_min = export_sequence.frame_min
|
||||||
|
frame_max = export_sequence.frame_max
|
||||||
|
|
||||||
sequence_name = get_psa_sequence_name(action, options.should_use_original_sequence_names)
|
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
|
||||||
|
psa_sequence.frame_count = frame_max - frame_min + 1
|
||||||
sequence.name = bytes(sequence_name, encoding='windows-1252')
|
psa_sequence.frame_start_index = frame_start_index
|
||||||
sequence.frame_count = frame_max - frame_min + 1
|
psa_sequence.fps = context.scene.render.fps
|
||||||
sequence.frame_start_index = frame_start_index
|
|
||||||
sequence.fps = context.scene.render.fps
|
|
||||||
|
|
||||||
frame_count = frame_max - frame_min + 1
|
frame_count = frame_max - frame_min + 1
|
||||||
|
|
||||||
|
# Store the mute state of the NLA strips we need to mute so we can restore the state after we are done.
|
||||||
|
nla_strip_mute_statuses = {x: x.mute for x in export_sequence.nla_strips_to_be_muted}
|
||||||
|
for nla_strip in export_sequence.nla_strips_to_be_muted:
|
||||||
|
nla_strip.mute = True
|
||||||
|
|
||||||
for frame in range(frame_count):
|
for frame in range(frame_count):
|
||||||
context.scene.frame_set(frame_min + frame)
|
context.scene.frame_set(frame_min + frame)
|
||||||
|
|
||||||
@@ -143,15 +191,54 @@ class PsaBuilder(object):
|
|||||||
key.rotation.y = rotation.y
|
key.rotation.y = rotation.y
|
||||||
key.rotation.z = rotation.z
|
key.rotation.z = rotation.z
|
||||||
key.rotation.w = rotation.w
|
key.rotation.w = rotation.w
|
||||||
key.time = 1.0 / sequence.fps
|
key.time = 1.0 / psa_sequence.fps
|
||||||
|
|
||||||
psa.keys.append(key)
|
psa.keys.append(key)
|
||||||
|
|
||||||
frame_start_index += 1
|
export_sequence.bone_count = len(pose_bones)
|
||||||
|
export_sequence.track_time = frame_count
|
||||||
|
|
||||||
sequence.bone_count = len(pose_bones)
|
# Restore the mute state of the NLA strips we muted beforehand.
|
||||||
sequence.track_time = frame_count
|
for nla_strip, mute in nla_strip_mute_statuses.items():
|
||||||
|
nla_strip.mute = mute
|
||||||
|
|
||||||
psa.sequences[action.name] = sequence
|
frame_start_index += frame_count
|
||||||
|
|
||||||
|
psa.sequences[export_sequence.name] = psa_sequence
|
||||||
|
|
||||||
return psa
|
return psa
|
||||||
|
|
||||||
|
def get_timeline_marker_sequence_frame_ranges(self, object, context, options: PsaBuilderOptions) -> Dict:
|
||||||
|
# Timeline markers need to be sorted so that we can determine the sequence start and end positions.
|
||||||
|
sequence_frame_ranges = dict()
|
||||||
|
sorted_timeline_markers = list(sorted(context.scene.timeline_markers, key=lambda x: x.frame))
|
||||||
|
sorted_timeline_marker_names = list(map(lambda x: x.name, sorted_timeline_markers))
|
||||||
|
|
||||||
|
for marker_name in options.marker_names:
|
||||||
|
marker = context.scene.timeline_markers[marker_name]
|
||||||
|
frame_min = marker.frame
|
||||||
|
# Determine the final frame of the sequence based on the next marker.
|
||||||
|
# If no subsequent marker exists, use the maximum frame_end from all NLA strips.
|
||||||
|
marker_index = sorted_timeline_marker_names.index(marker_name)
|
||||||
|
next_marker_index = marker_index + 1
|
||||||
|
frame_max = 0
|
||||||
|
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_max = sorted_timeline_markers[next_marker_index].frame
|
||||||
|
if options.should_trim_timeline_marker_sequences:
|
||||||
|
nla_strips = get_nla_strips_in_timeframe(object, marker.frame, frame_max)
|
||||||
|
frame_max = min(frame_max, max(map(lambda x: x.frame_end, nla_strips)))
|
||||||
|
frame_min = max(frame_min, min(map(lambda x: x.frame_start, nla_strips)))
|
||||||
|
else:
|
||||||
|
# There is no next marker.
|
||||||
|
# Find the final frame of all the NLA strips and use that as the last frame of this sequence.
|
||||||
|
for nla_track in object.animation_data.nla_tracks:
|
||||||
|
for strip in nla_track.strips:
|
||||||
|
frame_max = max(frame_max, strip.frame_end)
|
||||||
|
|
||||||
|
if frame_min == frame_max:
|
||||||
|
continue
|
||||||
|
|
||||||
|
sequence_frame_ranges[marker_name] = int(frame_min), int(frame_max)
|
||||||
|
|
||||||
|
return sequence_frame_ranges
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup, Panel
|
from bpy.types import Operator, PropertyGroup, Action, UIList, BoneGroup, Panel, TimelineMarker
|
||||||
from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty, EnumProperty
|
from bpy.props import CollectionProperty, IntProperty, PointerProperty, StringProperty, BoolProperty, EnumProperty
|
||||||
from bpy_extras.io_utils import ExportHelper
|
from bpy_extras.io_utils import ExportHelper
|
||||||
from typing import Type
|
from typing import Type
|
||||||
@@ -46,6 +46,16 @@ class PsaExportActionListItem(PropertyGroup):
|
|||||||
return self.action.name
|
return self.action.name
|
||||||
|
|
||||||
|
|
||||||
|
class PsaExportTimelineMarkerListItem(PropertyGroup):
|
||||||
|
marker_index: IntProperty()
|
||||||
|
marker_name: StringProperty()
|
||||||
|
is_selected: BoolProperty(default=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.marker_name
|
||||||
|
|
||||||
|
|
||||||
def update_action_names(context):
|
def update_action_names(context):
|
||||||
pg = context.scene.psa_export
|
pg = context.scene.psa_export
|
||||||
for item in pg.action_list:
|
for item in pg.action_list:
|
||||||
@@ -58,18 +68,28 @@ def should_use_original_sequence_names_updated(property, context):
|
|||||||
|
|
||||||
|
|
||||||
class PsaExportPropertyGroup(PropertyGroup):
|
class PsaExportPropertyGroup(PropertyGroup):
|
||||||
|
sequence_source: EnumProperty(
|
||||||
|
name='Source',
|
||||||
|
description='',
|
||||||
|
items=(
|
||||||
|
('ACTIONS', 'Actions', 'Sequences will be exported using actions'),
|
||||||
|
('TIMELINE_MARKERS', 'Timeline Markers', 'Sequences will be exported using timeline markers'),
|
||||||
|
)
|
||||||
|
)
|
||||||
action_list: CollectionProperty(type=PsaExportActionListItem)
|
action_list: CollectionProperty(type=PsaExportActionListItem)
|
||||||
action_list_index: IntProperty(default=0)
|
action_list_index: IntProperty(default=0)
|
||||||
|
marker_list: CollectionProperty(type=PsaExportTimelineMarkerListItem)
|
||||||
|
marker_list_index: IntProperty(default=0)
|
||||||
bone_filter_mode: EnumProperty(
|
bone_filter_mode: EnumProperty(
|
||||||
name='Bone Filter',
|
name='Bone Filter',
|
||||||
description='',
|
description='',
|
||||||
items=(
|
items=(
|
||||||
('ALL', 'All', 'All bones will be exported.'),
|
('ALL', 'All', 'All bones will be exported.'),
|
||||||
('BONE_GROUPS', 'Bone Groups', 'Only bones belonging to the selected bone groups and their ancestors will be exported.')
|
('BONE_GROUPS', 'Bone Groups', 'Only bones belonging to the selected bone groups and their ancestors will be exported.'),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
bone_group_list: CollectionProperty(type=BoneGroupListItem)
|
bone_group_list: CollectionProperty(type=BoneGroupListItem)
|
||||||
bone_group_list_index: IntProperty(default=0)
|
bone_group_list_index: IntProperty(default=0, name='', description='')
|
||||||
should_use_original_sequence_names: BoolProperty(default=False, name='Original Names', description='If the action was imported from the PSA Import panel, the original name of the sequence will be used instead of the Blender action name', update=should_use_original_sequence_names_updated)
|
should_use_original_sequence_names: BoolProperty(default=False, name='Original Names', description='If the action was imported from the PSA Import panel, the original name of the sequence will be used instead of the Blender action name', update=should_use_original_sequence_names_updated)
|
||||||
|
|
||||||
|
|
||||||
@@ -84,6 +104,7 @@ def is_bone_filter_mode_item_available(context, identifier):
|
|||||||
class PsaExportOperator(Operator, ExportHelper):
|
class PsaExportOperator(Operator, ExportHelper):
|
||||||
bl_idname = 'psa_export.operator'
|
bl_idname = 'psa_export.operator'
|
||||||
bl_label = 'Export'
|
bl_label = 'Export'
|
||||||
|
bl_options = {'INTERNAL', 'UNDO'}
|
||||||
__doc__ = 'Export actions to PSA'
|
__doc__ = 'Export actions to PSA'
|
||||||
filename_ext = '.psa'
|
filename_ext = '.psa'
|
||||||
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
|
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
|
||||||
@@ -100,7 +121,11 @@ class PsaExportOperator(Operator, ExportHelper):
|
|||||||
layout = self.layout
|
layout = self.layout
|
||||||
pg = context.scene.psa_export
|
pg = context.scene.psa_export
|
||||||
|
|
||||||
|
# SOURCE
|
||||||
|
layout.prop(pg, 'sequence_source', text='Source')
|
||||||
|
|
||||||
# ACTIONS
|
# ACTIONS
|
||||||
|
if pg.sequence_source == 'ACTIONS':
|
||||||
layout.label(text='Actions', icon='ACTION')
|
layout.label(text='Actions', icon='ACTION')
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
row.label(text='Select')
|
row.label(text='Select')
|
||||||
@@ -114,10 +139,16 @@ class PsaExportOperator(Operator, ExportHelper):
|
|||||||
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_original_sequence_names')
|
col.prop(pg, 'should_use_original_sequence_names')
|
||||||
|
elif pg.sequence_source == 'TIMELINE_MARKERS':
|
||||||
|
layout.label(text='Markers', icon='MARKER')
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
|
rows = max(3, min(len(pg.marker_list), 10))
|
||||||
|
row.template_list('PSA_UL_ExportTimelineMarkerList', '', pg, 'marker_list', pg, 'marker_list_index', rows=rows)
|
||||||
|
|
||||||
# Determine if there is going to be a naming conflict and display an error, if so.
|
# Determine if there is going to be a naming conflict and display an error, if so.
|
||||||
selected_actions = [x for x in pg.action_list if x.is_selected]
|
selected_items = [x for x in pg.action_list if x.is_selected]
|
||||||
action_names = [x.action_name for x in selected_actions]
|
action_names = [x.action_name for x in selected_items]
|
||||||
action_name_counts = Counter(action_names)
|
action_name_counts = Counter(action_names)
|
||||||
for action_name, count in action_name_counts.items():
|
for action_name, count in action_name_counts.items():
|
||||||
if count > 1:
|
if count > 1:
|
||||||
@@ -180,9 +211,15 @@ class PsaExportOperator(Operator, ExportHelper):
|
|||||||
|
|
||||||
update_action_names(context)
|
update_action_names(context)
|
||||||
|
|
||||||
if len(pg.action_list) == 0:
|
# Populate timeline markers list.
|
||||||
|
pg.marker_list.clear()
|
||||||
|
for marker in context.scene.timeline_markers:
|
||||||
|
item = pg.marker_list.add()
|
||||||
|
item.marker_name = marker.name
|
||||||
|
|
||||||
|
if len(pg.action_list) == 0 and len(pg.marker_names) == 0:
|
||||||
# If there are no actions at all, we have nothing to export, so just cancel the operation.
|
# If there are no actions at all, we have nothing to export, so just cancel the operation.
|
||||||
self.report({'ERROR_INVALID_CONTEXT'}, 'There are no actions to export.')
|
self.report({'ERROR_INVALID_CONTEXT'}, 'There are no actions or timeline markers to export.')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
# Populate bone groups list.
|
# Populate bone groups list.
|
||||||
@@ -194,28 +231,51 @@ class PsaExportOperator(Operator, ExportHelper):
|
|||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
pg = context.scene.psa_export
|
pg = context.scene.psa_export
|
||||||
actions = [x.action for x in pg.action_list if x.is_selected]
|
|
||||||
|
|
||||||
if len(actions) == 0:
|
actions = [x.action for x in pg.action_list if x.is_selected]
|
||||||
self.report({'ERROR_INVALID_CONTEXT'}, 'No actions were selected for export.')
|
marker_names = [x.marker_name for x in pg.marker_list if x.is_selected]
|
||||||
return {'CANCELLED'}
|
|
||||||
|
|
||||||
options = PsaBuilderOptions()
|
options = PsaBuilderOptions()
|
||||||
|
options.sequence_source = pg.sequence_source
|
||||||
options.actions = actions
|
options.actions = actions
|
||||||
|
options.marker_names = marker_names
|
||||||
options.bone_filter_mode = pg.bone_filter_mode
|
options.bone_filter_mode = pg.bone_filter_mode
|
||||||
options.bone_group_indices = [x.index for x in pg.bone_group_list if x.is_selected]
|
options.bone_group_indices = [x.index for x in pg.bone_group_list if x.is_selected]
|
||||||
options.should_use_original_sequence_names = pg.should_use_original_sequence_names
|
options.should_use_original_sequence_names = pg.should_use_original_sequence_names
|
||||||
builder = PsaBuilder()
|
builder = PsaBuilder()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
psa = builder.build(context, options)
|
psa = builder.build(context, options)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
|
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
exporter = PsaExporter(psa)
|
exporter = PsaExporter(psa)
|
||||||
exporter.export(self.filepath)
|
exporter.export(self.filepath)
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class PSA_UL_ExportTimelineMarkerList(UIList):
|
||||||
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||||
|
layout.alignment = 'LEFT'
|
||||||
|
layout.prop(item, 'is_selected', icon_only=True)
|
||||||
|
layout.label(text=item.marker_name)
|
||||||
|
|
||||||
|
def filter_items(self, context, data, property):
|
||||||
|
actions = getattr(data, property)
|
||||||
|
flt_flags = []
|
||||||
|
flt_neworder = []
|
||||||
|
if self.filter_name:
|
||||||
|
flt_flags = bpy.types.UI_UL_list.filter_items_by_name(
|
||||||
|
self.filter_name,
|
||||||
|
self.bitflag_filter_item,
|
||||||
|
actions,
|
||||||
|
'marker_name',
|
||||||
|
reverse=self.use_filter_invert
|
||||||
|
)
|
||||||
|
return flt_flags, flt_neworder
|
||||||
|
|
||||||
|
|
||||||
class PSA_UL_ExportActionList(UIList):
|
class PSA_UL_ExportActionList(UIList):
|
||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||||
layout.alignment = 'LEFT'
|
layout.alignment = 'LEFT'
|
||||||
@@ -237,17 +297,18 @@ class PSA_UL_ExportActionList(UIList):
|
|||||||
return flt_flags, flt_neworder
|
return flt_flags, flt_neworder
|
||||||
|
|
||||||
|
|
||||||
class PsaExportSelectAll(bpy.types.Operator):
|
class PsaExportSelectAll(Operator):
|
||||||
bl_idname = 'psa_export.actions_select_all'
|
bl_idname = 'psa_export.actions_select_all'
|
||||||
bl_label = 'Select All'
|
bl_label = 'Select All'
|
||||||
bl_description = 'Select all actions'
|
bl_description = 'Select all actions'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
pg = context.scene.psa_export
|
pg = context.scene.psa_export
|
||||||
action_list = pg.action_list
|
item_list = pg.action_list
|
||||||
has_unselected_actions = any(map(lambda action: not action.is_selected, action_list))
|
has_unselected_actions = any(map(lambda action: not action.is_selected, item_list))
|
||||||
return len(action_list) > 0 and has_unselected_actions
|
return len(item_list) > 0 and has_unselected_actions
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
pg = context.scene.psa_export
|
pg = context.scene.psa_export
|
||||||
@@ -256,17 +317,18 @@ class PsaExportSelectAll(bpy.types.Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class PsaExportDeselectAll(bpy.types.Operator):
|
class PsaExportDeselectAll(Operator):
|
||||||
bl_idname = 'psa_export.actions_deselect_all'
|
bl_idname = 'psa_export.actions_deselect_all'
|
||||||
bl_label = 'Deselect All'
|
bl_label = 'Deselect All'
|
||||||
bl_description = 'Deselect all actions'
|
bl_description = 'Deselect all actions'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
pg = context.scene.psa_export
|
pg = context.scene.psa_export
|
||||||
action_list = pg.action_list
|
item_list = pg.action_list
|
||||||
has_selected_actions = any(map(lambda action: action.is_selected, action_list))
|
has_selected_actions = any(map(lambda action: action.is_selected, item_list))
|
||||||
return len(action_list) > 0 and has_selected_actions
|
return len(item_list) > 0 and has_selected_actions
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
pg = context.scene.psa_export
|
pg = context.scene.psa_export
|
||||||
@@ -277,9 +339,11 @@ class PsaExportDeselectAll(bpy.types.Operator):
|
|||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
PsaExportActionListItem,
|
PsaExportActionListItem,
|
||||||
|
PsaExportTimelineMarkerListItem,
|
||||||
PsaExportPropertyGroup,
|
PsaExportPropertyGroup,
|
||||||
PsaExportOperator,
|
PsaExportOperator,
|
||||||
PSA_UL_ExportActionList,
|
PSA_UL_ExportActionList,
|
||||||
|
PSA_UL_ExportTimelineMarkerList,
|
||||||
PsaExportSelectAll,
|
PsaExportSelectAll,
|
||||||
PsaExportDeselectAll,
|
PsaExportDeselectAll,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -293,10 +293,11 @@ class PSA_UL_ImportActionList(PSA_UL_SequenceList, UIList):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PsaImportSequencesSelectAll(bpy.types.Operator):
|
class PsaImportSequencesSelectAll(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'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -312,10 +313,11 @@ class PsaImportSequencesSelectAll(bpy.types.Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class PsaImportActionsSelectAll(bpy.types.Operator):
|
class PsaImportActionsSelectAll(Operator):
|
||||||
bl_idname = 'psa_import.actions_select_all'
|
bl_idname = 'psa_import.actions_select_all'
|
||||||
bl_label = 'All'
|
bl_label = 'All'
|
||||||
bl_description = 'Select all actions'
|
bl_description = 'Select all actions'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -331,10 +333,11 @@ class PsaImportActionsSelectAll(bpy.types.Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class PsaImportSequencesDeselectAll(bpy.types.Operator):
|
class PsaImportSequencesDeselectAll(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 sequences'
|
bl_description = 'Deselect all sequences'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -350,10 +353,11 @@ class PsaImportSequencesDeselectAll(bpy.types.Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class PsaImportActionsDeselectAll(bpy.types.Operator):
|
class PsaImportActionsDeselectAll(Operator):
|
||||||
bl_idname = 'psa_import.actions_deselect_all'
|
bl_idname = 'psa_import.actions_deselect_all'
|
||||||
bl_label = 'None'
|
bl_label = 'None'
|
||||||
bl_description = 'Deselect all actions'
|
bl_description = 'Deselect all actions'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -406,7 +410,7 @@ class PSA_PT_ImportPanel_PsaData(Panel):
|
|||||||
pg = context.scene.psa_import.psa
|
pg = context.scene.psa_import.psa
|
||||||
|
|
||||||
layout.label(text=f'{len(pg.bones)} Bones', icon='BONE_DATA')
|
layout.label(text=f'{len(pg.bones)} Bones', icon='BONE_DATA')
|
||||||
layout.label(text=f'{len(pg.sequence_count)} Sequences', icon='SEQUENCE')
|
layout.label(text=f'{pg.sequence_count} Sequences', icon='SEQUENCE')
|
||||||
|
|
||||||
|
|
||||||
class PSA_PT_ImportPanel(Panel):
|
class PSA_PT_ImportPanel(Panel):
|
||||||
@@ -469,7 +473,7 @@ class PSA_PT_ImportPanel(Panel):
|
|||||||
class PsaImportFileReload(Operator):
|
class PsaImportFileReload(Operator):
|
||||||
bl_idname = 'psa_import.file_reload'
|
bl_idname = 'psa_import.file_reload'
|
||||||
bl_label = 'Refresh'
|
bl_label = 'Refresh'
|
||||||
bl_options = {'REGISTER'}
|
bl_options = {'INTERNAL'}
|
||||||
bl_description = 'Refresh the PSA file'
|
bl_description = 'Refresh the PSA file'
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
@@ -480,7 +484,7 @@ class PsaImportFileReload(Operator):
|
|||||||
class PsaImportSelectFile(Operator):
|
class PsaImportSelectFile(Operator):
|
||||||
bl_idname = 'psa_import.select_file'
|
bl_idname = 'psa_import.select_file'
|
||||||
bl_label = 'Select'
|
bl_label = 'Select'
|
||||||
bl_options = {'REGISTER', 'UNDO'}
|
bl_options = {'INTERNAL'}
|
||||||
bl_description = 'Select a PSA file from which to import animations'
|
bl_description = 'Select a PSA file from which to import animations'
|
||||||
filepath: bpy.props.StringProperty(subtype='FILE_PATH')
|
filepath: bpy.props.StringProperty(subtype='FILE_PATH')
|
||||||
filter_glob: bpy.props.StringProperty(default="*.psa", options={'HIDDEN'})
|
filter_glob: bpy.props.StringProperty(default="*.psa", options={'HIDDEN'})
|
||||||
@@ -498,6 +502,7 @@ class PsaImportOperator(Operator):
|
|||||||
bl_idname = 'psa_import.import'
|
bl_idname = 'psa_import.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'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -524,6 +529,7 @@ class PsaImportOperator(Operator):
|
|||||||
class PsaImportPushToActions(Operator):
|
class PsaImportPushToActions(Operator):
|
||||||
bl_idname = 'psa_import.push_to_actions'
|
bl_idname = 'psa_import.push_to_actions'
|
||||||
bl_label = 'Push to Actions'
|
bl_label = 'Push to Actions'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -547,6 +553,7 @@ class PsaImportPushToActions(Operator):
|
|||||||
class PsaImportPopFromActions(Operator):
|
class PsaImportPopFromActions(Operator):
|
||||||
bl_idname = 'psa_import.pop_from_actions'
|
bl_idname = 'psa_import.pop_from_actions'
|
||||||
bl_label = 'Pop From Actions'
|
bl_label = 'Pop From Actions'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
@@ -570,6 +577,7 @@ class PsaImportPopFromActions(Operator):
|
|||||||
class PsaImportFileSelectOperator(Operator, ImportHelper):
|
class PsaImportFileSelectOperator(Operator, ImportHelper):
|
||||||
bl_idname = 'psa_import.file_select'
|
bl_idname = 'psa_import.file_select'
|
||||||
bl_label = 'File Select'
|
bl_label = 'File Select'
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
filename_ext = '.psa'
|
filename_ext = '.psa'
|
||||||
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
|
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
|
||||||
filepath: StringProperty(
|
filepath: StringProperty(
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class PskBuilder(object):
|
|||||||
# If the mesh has no armature object, simply assign it a dummy bone at the root to satisfy the requirement
|
# If the mesh has no armature object, simply assign it a dummy bone at the root to satisfy the requirement
|
||||||
# that a PSK file must have at least one bone.
|
# that a PSK file must have at least one bone.
|
||||||
psk_bone = Psk.Bone()
|
psk_bone = Psk.Bone()
|
||||||
psk_bone.name = bytes('static', encoding='utf-8')
|
psk_bone.name = bytes('static', encoding='windows-1252')
|
||||||
psk_bone.flags = 0
|
psk_bone.flags = 0
|
||||||
psk_bone.children_count = 0
|
psk_bone.children_count = 0
|
||||||
psk_bone.parent_index = 0
|
psk_bone.parent_index = 0
|
||||||
@@ -88,8 +88,6 @@ class PskBuilder(object):
|
|||||||
|
|
||||||
# Ensure that the exported hierarchy has a single root bone.
|
# Ensure that the exported hierarchy has a single root bone.
|
||||||
root_bones = [x for x in bones if x.parent is None]
|
root_bones = [x for x in bones if x.parent is None]
|
||||||
print('root bones')
|
|
||||||
print(root_bones)
|
|
||||||
if len(root_bones) > 1:
|
if len(root_bones) > 1:
|
||||||
root_bone_names = [x.name for x in root_bones]
|
root_bone_names = [x.name for x in root_bones]
|
||||||
raise RuntimeError('Exported bone hierarchy must have a single root bone.'
|
raise RuntimeError('Exported bone hierarchy must have a single root bone.'
|
||||||
@@ -97,7 +95,7 @@ class PskBuilder(object):
|
|||||||
|
|
||||||
for bone in bones:
|
for bone in bones:
|
||||||
psk_bone = Psk.Bone()
|
psk_bone = Psk.Bone()
|
||||||
psk_bone.name = bytes(bone.name, encoding='utf-8')
|
psk_bone.name = bytes(bone.name, encoding='windows-1252')
|
||||||
psk_bone.flags = 0
|
psk_bone.flags = 0
|
||||||
psk_bone.children_count = 0
|
psk_bone.children_count = 0
|
||||||
|
|
||||||
@@ -133,9 +131,9 @@ class PskBuilder(object):
|
|||||||
|
|
||||||
psk.bones.append(psk_bone)
|
psk.bones.append(psk_bone)
|
||||||
|
|
||||||
vertex_offset = 0
|
|
||||||
|
|
||||||
for object in input_objects.mesh_objects:
|
for object in input_objects.mesh_objects:
|
||||||
|
vertex_offset = len(psk.points)
|
||||||
|
|
||||||
# VERTICES
|
# VERTICES
|
||||||
for vertex in object.data.vertices:
|
for vertex in object.data.vertices:
|
||||||
point = Vector3()
|
point = Vector3()
|
||||||
@@ -153,8 +151,10 @@ class PskBuilder(object):
|
|||||||
if m is None:
|
if m is None:
|
||||||
raise RuntimeError('Material cannot be empty (index ' + str(i) + ')')
|
raise RuntimeError('Material cannot be empty (index ' + str(i) + ')')
|
||||||
if m.name in materials:
|
if m.name in materials:
|
||||||
|
# Material already evaluated, just get its index.
|
||||||
material_index = list(materials.keys()).index(m.name)
|
material_index = list(materials.keys()).index(m.name)
|
||||||
else:
|
else:
|
||||||
|
# New material.
|
||||||
material = Psk.Material()
|
material = Psk.Material()
|
||||||
material.name = bytes(m.name, encoding='utf-8')
|
material.name = bytes(m.name, encoding='utf-8')
|
||||||
material.texture_index = len(psk.materials)
|
material.texture_index = len(psk.materials)
|
||||||
@@ -230,9 +230,9 @@ class PskBuilder(object):
|
|||||||
bone = bone.parent
|
bone = bone.parent
|
||||||
for vertex_group_index, vertex_group in enumerate(object.vertex_groups):
|
for vertex_group_index, vertex_group in enumerate(object.vertex_groups):
|
||||||
if vertex_group_index not in vertex_group_bone_indices:
|
if vertex_group_index not in vertex_group_bone_indices:
|
||||||
|
# Vertex group has no associated bone, skip it.
|
||||||
continue
|
continue
|
||||||
bone_index = vertex_group_bone_indices[vertex_group_index]
|
bone_index = vertex_group_bone_indices[vertex_group_index]
|
||||||
# TODO: exclude vertex group if it doesn't match to a bone we are exporting
|
|
||||||
for vertex_index in range(len(object.data.vertices)):
|
for vertex_index in range(len(object.data.vertices)):
|
||||||
try:
|
try:
|
||||||
weight = vertex_group.weight(vertex_index)
|
weight = vertex_group.weight(vertex_index)
|
||||||
@@ -246,6 +246,4 @@ class PskBuilder(object):
|
|||||||
w.weight = weight
|
w.weight = weight
|
||||||
psk.weights.append(w)
|
psk.weights.append(w)
|
||||||
|
|
||||||
vertex_offset = len(psk.points)
|
|
||||||
|
|
||||||
return psk
|
return psk
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ def is_bone_filter_mode_item_available(context, identifier):
|
|||||||
class PskExportOperator(Operator, ExportHelper):
|
class PskExportOperator(Operator, ExportHelper):
|
||||||
bl_idname = 'export.psk'
|
bl_idname = 'export.psk'
|
||||||
bl_label = 'Export'
|
bl_label = 'Export'
|
||||||
|
bl_options = {'INTERNAL', 'UNDO'}
|
||||||
__doc__ = 'Export mesh and armature to PSK'
|
__doc__ = 'Export mesh and armature to PSK'
|
||||||
filename_ext = '.psk'
|
filename_ext = '.psk'
|
||||||
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})
|
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ class PskImportPropertyGroup(PropertyGroup):
|
|||||||
class PskImportOperator(Operator, ImportHelper):
|
class PskImportOperator(Operator, ImportHelper):
|
||||||
bl_idname = 'import.psk'
|
bl_idname = 'import.psk'
|
||||||
bl_label = 'Export'
|
bl_label = 'Export'
|
||||||
|
bl_options = {'INTERNAL', 'UNDO'}
|
||||||
__doc__ = 'Load a PSK file'
|
__doc__ = 'Load a PSK file'
|
||||||
filename_ext = '.psk'
|
filename_ext = '.psk'
|
||||||
filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'})
|
filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'})
|
||||||
@@ -276,7 +277,6 @@ class PskImportOperator(Operator, ImportHelper):
|
|||||||
layout.prop(pg, 'vertex_color_space')
|
layout.prop(pg, 'vertex_color_space')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
PskImportOperator,
|
PskImportOperator,
|
||||||
PskImportPropertyGroup,
|
PskImportPropertyGroup,
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ from bpy.props import StringProperty, IntProperty, BoolProperty
|
|||||||
|
|
||||||
class PSX_UL_BoneGroupList(UIList):
|
class PSX_UL_BoneGroupList(UIList):
|
||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
||||||
layout.alignment = 'LEFT'
|
row = layout.row()
|
||||||
layout.prop(item, 'is_selected', icon_only=True)
|
row.prop(item, 'is_selected', text=item.name)
|
||||||
layout.label(text=item.name, icon='GROUP_BONE' if item.index >= 0 else 'NONE')
|
row.label(text=str(item.count), icon='BONE_DATA')
|
||||||
|
|
||||||
|
|
||||||
class BoneGroupListItem(PropertyGroup):
|
class BoneGroupListItem(PropertyGroup):
|
||||||
name: StringProperty()
|
name: StringProperty()
|
||||||
index: IntProperty()
|
index: IntProperty()
|
||||||
|
count: IntProperty()
|
||||||
is_selected: BoolProperty(default=False)
|
is_selected: BoolProperty(default=False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
Reference in New Issue
Block a user