1
0
mirror of https://github.com/DarklightGames/io_scene_psk_psa.git synced 2024-11-15 02:37:39 +01:00

Added the ability to export sequences using timeline markers (WIP, not thoroughly tested yet!)

A bunch of clean up
This commit is contained in:
Colin Basnett 2022-02-11 15:21:31 -08:00
parent b58b44cafb
commit 7ad8f0238a
8 changed files with 262 additions and 75 deletions

View File

@ -1,4 +1,6 @@
from bpy.types import NlaStrip
from typing import List
from collections import Counter
def rgb_to_srgb(c):
@ -8,19 +10,45 @@ def rgb_to_srgb(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):
bone_group_list.clear()
item = bone_group_list.add()
item.name = '(unassigned)'
item.index = -1
item.is_selected = True
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.name = 'Unassigned'
item.index = -1
item.count = 0 if None not in bone_group_counts else bone_group_counts[None]
item.is_selected = True
for bone_group_index, bone_group in enumerate(armature_object.pose.bone_groups):
item = bone_group_list.add()
item.name = bone_group.name
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

View File

@ -1,13 +1,17 @@
from .data import *
from ..helpers import *
from typing import Dict
class PsaBuilderOptions(object):
def __init__(self):
self.sequence_source = 'ACTIONS'
self.actions = []
self.marker_names = []
self.bone_filter_mode = 'ALL'
self.bone_group_indices = []
self.should_use_original_sequence_names = False
self.should_trim_timeline_marker_sequences = True
class PsaBuilder(object):
@ -25,6 +29,12 @@ class PsaBuilder(object):
if armature.animation_data is None:
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()
bones = list(armature.data.bones)
@ -59,6 +69,7 @@ class PsaBuilder(object):
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}')
# Build list of PSA bones.
for pose_bone in bones:
psa_bone = Psa.Bone()
psa_bone.name = bytes(pose_bone.name, encoding='utf-8')
@ -95,28 +106,65 @@ class PsaBuilder(object):
psa.bones.append(psa_bone)
# 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:
if len(action.fcurves) == 0:
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}')
frame_start_index = 0
for action in options.actions:
if len(action.fcurves) == 0:
continue
armature.animation_data.action = action
# 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()
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)
sequence.name = bytes(sequence_name, encoding='windows-1252')
sequence.frame_count = frame_max - frame_min + 1
sequence.frame_start_index = frame_start_index
sequence.fps = context.scene.render.fps
psa_sequence.name = bytes(export_sequence.name, encoding='windows-1252')
psa_sequence.frame_count = frame_max - frame_min + 1
psa_sequence.frame_start_index = frame_start_index
psa_sequence.fps = context.scene.render.fps
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):
context.scene.frame_set(frame_min + frame)
@ -143,15 +191,54 @@ class PsaBuilder(object):
key.rotation.y = rotation.y
key.rotation.z = rotation.z
key.rotation.w = rotation.w
key.time = 1.0 / sequence.fps
key.time = 1.0 / psa_sequence.fps
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)
sequence.track_time = frame_count
# Restore the mute state of the NLA strips we muted beforehand.
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
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

View File

@ -1,5 +1,5 @@
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_extras.io_utils import ExportHelper
from typing import Type
@ -46,6 +46,16 @@ class PsaExportActionListItem(PropertyGroup):
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):
pg = context.scene.psa_export
for item in pg.action_list:
@ -58,18 +68,28 @@ def should_use_original_sequence_names_updated(property, context):
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_index: IntProperty(default=0)
marker_list: CollectionProperty(type=PsaExportTimelineMarkerListItem)
marker_list_index: IntProperty(default=0)
bone_filter_mode: EnumProperty(
name='Bone Filter',
description='',
items=(
('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_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)
@ -84,6 +104,7 @@ def is_bone_filter_mode_item_available(context, identifier):
class PsaExportOperator(Operator, ExportHelper):
bl_idname = 'psa_export.operator'
bl_label = 'Export'
bl_options = {'INTERNAL', 'UNDO'}
__doc__ = 'Export actions to PSA'
filename_ext = '.psa'
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
@ -100,24 +121,34 @@ class PsaExportOperator(Operator, ExportHelper):
layout = self.layout
pg = context.scene.psa_export
# ACTIONS
layout.label(text='Actions', icon='ACTION')
row = layout.row(align=True)
row.label(text='Select')
row.operator(PsaExportSelectAll.bl_idname, text='All')
row.operator(PsaExportDeselectAll.bl_idname, text='None')
row = layout.row()
rows = max(3, min(len(pg.action_list), 10))
row.template_list('PSA_UL_ExportActionList', '', pg, 'action_list', pg, 'action_list_index', rows=rows)
# SOURCE
layout.prop(pg, 'sequence_source', text='Source')
col = layout.column(heading="Options")
col.use_property_split = True
col.use_property_decorate = False
col.prop(pg, 'should_use_original_sequence_names')
# ACTIONS
if pg.sequence_source == 'ACTIONS':
layout.label(text='Actions', icon='ACTION')
row = layout.row(align=True)
row.label(text='Select')
row.operator(PsaExportSelectAll.bl_idname, text='All')
row.operator(PsaExportDeselectAll.bl_idname, text='None')
row = layout.row()
rows = max(3, min(len(pg.action_list), 10))
row.template_list('PSA_UL_ExportActionList', '', pg, 'action_list', pg, 'action_list_index', rows=rows)
col = layout.column(heading="Options")
col.use_property_split = True
col.use_property_decorate = False
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.
selected_actions = [x for x in pg.action_list if x.is_selected]
action_names = [x.action_name for x in selected_actions]
selected_items = [x for x in pg.action_list if x.is_selected]
action_names = [x.action_name for x in selected_items]
action_name_counts = Counter(action_names)
for action_name, count in action_name_counts.items():
if count > 1:
@ -180,9 +211,15 @@ class PsaExportOperator(Operator, ExportHelper):
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.
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'}
# Populate bone groups list.
@ -194,28 +231,51 @@ class PsaExportOperator(Operator, ExportHelper):
def execute(self, context):
pg = context.scene.psa_export
actions = [x.action for x in pg.action_list if x.is_selected]
if len(actions) == 0:
self.report({'ERROR_INVALID_CONTEXT'}, 'No actions were selected for export.')
return {'CANCELLED'}
actions = [x.action for x in pg.action_list if x.is_selected]
marker_names = [x.marker_name for x in pg.marker_list if x.is_selected]
options = PsaBuilderOptions()
options.sequence_source = pg.sequence_source
options.actions = actions
options.marker_names = marker_names
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.should_use_original_sequence_names = pg.should_use_original_sequence_names
builder = PsaBuilder()
try:
psa = builder.build(context, options)
except RuntimeError as e:
self.report({'ERROR_INVALID_CONTEXT'}, str(e))
return {'CANCELLED'}
exporter = PsaExporter(psa)
exporter.export(self.filepath)
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):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
layout.alignment = 'LEFT'
@ -237,17 +297,18 @@ class PSA_UL_ExportActionList(UIList):
return flt_flags, flt_neworder
class PsaExportSelectAll(bpy.types.Operator):
class PsaExportSelectAll(Operator):
bl_idname = 'psa_export.actions_select_all'
bl_label = 'Select All'
bl_description = 'Select all actions'
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
pg = context.scene.psa_export
action_list = pg.action_list
has_unselected_actions = any(map(lambda action: not action.is_selected, action_list))
return len(action_list) > 0 and has_unselected_actions
item_list = pg.action_list
has_unselected_actions = any(map(lambda action: not action.is_selected, item_list))
return len(item_list) > 0 and has_unselected_actions
def execute(self, context):
pg = context.scene.psa_export
@ -256,17 +317,18 @@ class PsaExportSelectAll(bpy.types.Operator):
return {'FINISHED'}
class PsaExportDeselectAll(bpy.types.Operator):
class PsaExportDeselectAll(Operator):
bl_idname = 'psa_export.actions_deselect_all'
bl_label = 'Deselect All'
bl_description = 'Deselect all actions'
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
pg = context.scene.psa_export
action_list = pg.action_list
has_selected_actions = any(map(lambda action: action.is_selected, action_list))
return len(action_list) > 0 and has_selected_actions
item_list = pg.action_list
has_selected_actions = any(map(lambda action: action.is_selected, item_list))
return len(item_list) > 0 and has_selected_actions
def execute(self, context):
pg = context.scene.psa_export
@ -277,9 +339,11 @@ class PsaExportDeselectAll(bpy.types.Operator):
classes = (
PsaExportActionListItem,
PsaExportTimelineMarkerListItem,
PsaExportPropertyGroup,
PsaExportOperator,
PSA_UL_ExportActionList,
PSA_UL_ExportTimelineMarkerList,
PsaExportSelectAll,
PsaExportDeselectAll,
)

View File

@ -293,10 +293,11 @@ class PSA_UL_ImportActionList(PSA_UL_SequenceList, UIList):
pass
class PsaImportSequencesSelectAll(bpy.types.Operator):
class PsaImportSequencesSelectAll(Operator):
bl_idname = 'psa_import.sequences_select_all'
bl_label = 'All'
bl_description = 'Select all sequences'
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
@ -312,10 +313,11 @@ class PsaImportSequencesSelectAll(bpy.types.Operator):
return {'FINISHED'}
class PsaImportActionsSelectAll(bpy.types.Operator):
class PsaImportActionsSelectAll(Operator):
bl_idname = 'psa_import.actions_select_all'
bl_label = 'All'
bl_description = 'Select all actions'
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
@ -331,10 +333,11 @@ class PsaImportActionsSelectAll(bpy.types.Operator):
return {'FINISHED'}
class PsaImportSequencesDeselectAll(bpy.types.Operator):
class PsaImportSequencesDeselectAll(Operator):
bl_idname = 'psa_import.sequences_deselect_all'
bl_label = 'None'
bl_description = 'Deselect all sequences'
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
@ -350,10 +353,11 @@ class PsaImportSequencesDeselectAll(bpy.types.Operator):
return {'FINISHED'}
class PsaImportActionsDeselectAll(bpy.types.Operator):
class PsaImportActionsDeselectAll(Operator):
bl_idname = 'psa_import.actions_deselect_all'
bl_label = 'None'
bl_description = 'Deselect all actions'
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
@ -406,7 +410,7 @@ class PSA_PT_ImportPanel_PsaData(Panel):
pg = context.scene.psa_import.psa
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):
@ -469,7 +473,7 @@ class PSA_PT_ImportPanel(Panel):
class PsaImportFileReload(Operator):
bl_idname = 'psa_import.file_reload'
bl_label = 'Refresh'
bl_options = {'REGISTER'}
bl_options = {'INTERNAL'}
bl_description = 'Refresh the PSA file'
def execute(self, context):
@ -480,7 +484,7 @@ class PsaImportFileReload(Operator):
class PsaImportSelectFile(Operator):
bl_idname = 'psa_import.select_file'
bl_label = 'Select'
bl_options = {'REGISTER', 'UNDO'}
bl_options = {'INTERNAL'}
bl_description = 'Select a PSA file from which to import animations'
filepath: bpy.props.StringProperty(subtype='FILE_PATH')
filter_glob: bpy.props.StringProperty(default="*.psa", options={'HIDDEN'})
@ -498,6 +502,7 @@ class PsaImportOperator(Operator):
bl_idname = 'psa_import.import'
bl_label = 'Import'
bl_description = 'Import the selected animations into the scene as actions'
bl_options = {'INTERNAL', 'UNDO'}
@classmethod
def poll(cls, context):
@ -524,6 +529,7 @@ class PsaImportOperator(Operator):
class PsaImportPushToActions(Operator):
bl_idname = 'psa_import.push_to_actions'
bl_label = 'Push to Actions'
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
@ -547,6 +553,7 @@ class PsaImportPushToActions(Operator):
class PsaImportPopFromActions(Operator):
bl_idname = 'psa_import.pop_from_actions'
bl_label = 'Pop From Actions'
bl_options = {'INTERNAL'}
@classmethod
def poll(cls, context):
@ -570,6 +577,7 @@ class PsaImportPopFromActions(Operator):
class PsaImportFileSelectOperator(Operator, ImportHelper):
bl_idname = 'psa_import.file_select'
bl_label = 'File Select'
bl_options = {'INTERNAL'}
filename_ext = '.psa'
filter_glob: StringProperty(default='*.psa', options={'HIDDEN'})
filepath: StringProperty(

View File

@ -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
# that a PSK file must have at least one 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.children_count = 0
psk_bone.parent_index = 0
@ -88,8 +88,6 @@ class PskBuilder(object):
# Ensure that the exported hierarchy has a single root bone.
root_bones = [x for x in bones if x.parent is None]
print('root bones')
print(root_bones)
if len(root_bones) > 1:
root_bone_names = [x.name for x in root_bones]
raise RuntimeError('Exported bone hierarchy must have a single root bone.'
@ -97,7 +95,7 @@ class PskBuilder(object):
for bone in bones:
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.children_count = 0
@ -133,9 +131,9 @@ class PskBuilder(object):
psk.bones.append(psk_bone)
vertex_offset = 0
for object in input_objects.mesh_objects:
vertex_offset = len(psk.points)
# VERTICES
for vertex in object.data.vertices:
point = Vector3()
@ -153,8 +151,10 @@ class PskBuilder(object):
if m is None:
raise RuntimeError('Material cannot be empty (index ' + str(i) + ')')
if m.name in materials:
# Material already evaluated, just get its index.
material_index = list(materials.keys()).index(m.name)
else:
# New material.
material = Psk.Material()
material.name = bytes(m.name, encoding='utf-8')
material.texture_index = len(psk.materials)
@ -230,9 +230,9 @@ class PskBuilder(object):
bone = bone.parent
for vertex_group_index, vertex_group in enumerate(object.vertex_groups):
if vertex_group_index not in vertex_group_bone_indices:
# Vertex group has no associated bone, skip it.
continue
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)):
try:
weight = vertex_group.weight(vertex_index)
@ -246,6 +246,4 @@ class PskBuilder(object):
w.weight = weight
psk.weights.append(w)
vertex_offset = len(psk.points)
return psk

View File

@ -73,6 +73,7 @@ def is_bone_filter_mode_item_available(context, identifier):
class PskExportOperator(Operator, ExportHelper):
bl_idname = 'export.psk'
bl_label = 'Export'
bl_options = {'INTERNAL', 'UNDO'}
__doc__ = 'Export mesh and armature to PSK'
filename_ext = '.psk'
filter_glob: StringProperty(default='*.psk', options={'HIDDEN'})

View File

@ -245,6 +245,7 @@ class PskImportPropertyGroup(PropertyGroup):
class PskImportOperator(Operator, ImportHelper):
bl_idname = 'import.psk'
bl_label = 'Export'
bl_options = {'INTERNAL', 'UNDO'}
__doc__ = 'Load a PSK file'
filename_ext = '.psk'
filter_glob: StringProperty(default='*.psk;*.pskx', options={'HIDDEN'})
@ -276,7 +277,6 @@ class PskImportOperator(Operator, ImportHelper):
layout.prop(pg, 'vertex_color_space')
classes = (
PskImportOperator,
PskImportPropertyGroup,

View File

@ -4,14 +4,15 @@ from bpy.props import StringProperty, IntProperty, BoolProperty
class PSX_UL_BoneGroupList(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.name, icon='GROUP_BONE' if item.index >= 0 else 'NONE')
row = layout.row()
row.prop(item, 'is_selected', text=item.name)
row.label(text=str(item.count), icon='BONE_DATA')
class BoneGroupListItem(PropertyGroup):
name: StringProperty()
index: IntProperty()
count: IntProperty()
is_selected: BoolProperty(default=False)
@property