diff --git a/io_scene_psk_psa/psa/builder.py b/io_scene_psk_psa/psa/builder.py index ca19c2c..6b24327 100644 --- a/io_scene_psk_psa/psa/builder.py +++ b/io_scene_psk_psa/psa/builder.py @@ -1,7 +1,7 @@ from bpy.types import Action, AnimData, Context, Object, PoseBone from psk_psa_py.psa.data import Psa -from typing import Dict, List, Optional, Tuple +from typing import Tuple from mathutils import Matrix, Quaternion, Vector from ..shared.helpers import PsxBoneCollection, convert_bpy_quaternion_to_psx_quaternion, convert_vector_to_vector3, create_psx_bones, get_coordinate_system_transform @@ -10,7 +10,7 @@ from ..shared.helpers import PsxBoneCollection, convert_bpy_quaternion_to_psx_qu class PsaBuildSequence: class NlaState: def __init__(self): - self.action: Optional[Action] = None + self.action: Action | None = None self.frame_start: int = 0 self.frame_end: int = 0 @@ -22,16 +22,16 @@ class PsaBuildSequence: self.compression_ratio: float = 1.0 self.key_quota: int = 0 self.fps: float = 30.0 - self.group: Optional[str] = None + self.group: str | None = None class PsaBuildOptions: def __init__(self): - self.armature_objects: List[Object] = [] - self.animation_data: Optional[AnimData] = None - self.sequences: List[PsaBuildSequence] = [] + self.armature_objects: list[Object] = [] + self.animation_data: AnimData | None = None + self.sequences: list[PsaBuildSequence] = [] self.bone_filter_mode: str = 'ALL' - self.bone_collection_indices: List[PsxBoneCollection] = [] + self.bone_collection_indices: list[PsxBoneCollection] = [] self.sequence_name_prefix: str = '' self.sequence_name_suffix: str = '' self.scale = 1.0 @@ -307,7 +307,6 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa: options.export_space, export_bone.scale, coordinate_system_transform=coordinate_system_transform - # has_false_root_bone=psx_bone_create_result.has_false_root_bone, ) last_frame_bone_poses.append((location, rotation)) @@ -331,7 +330,6 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa: export_space=options.export_space, scale=export_bone.scale, coordinate_system_transform=coordinate_system_transform, - # has_false_root_bone=psx_bone_create_result.has_false_root_bone, ) next_frame_bone_poses.append((location, rotation)) @@ -359,7 +357,6 @@ def build_psa(context: Context, options: PsaBuildOptions) -> Psa: export_space=options.export_space, scale=export_bone.scale, coordinate_system_transform=coordinate_system_transform, - # has_false_root_bone=psx_bone_create_result.has_false_root_bone, ) add_key(location, rotation) diff --git a/io_scene_psk_psa/psa/config.py b/io_scene_psk_psa/psa/config.py index 0d2404a..1a81584 100644 --- a/io_scene_psk_psa/psa/config.py +++ b/io_scene_psk_psa/psa/config.py @@ -1,6 +1,5 @@ import re from configparser import ConfigParser -from typing import Dict, List REMOVE_TRACK_LOCATION = (1 << 0) REMOVE_TRACK_ROTATION = (1 << 1) @@ -8,7 +7,7 @@ REMOVE_TRACK_ROTATION = (1 << 1) class PsaConfig: def __init__(self): - self.sequence_bone_flags: Dict[str, Dict[int, int]] = dict() + self.sequence_bone_flags: dict[str, dict[int, int]] = dict() def _load_config_file(file_path: str) -> ConfigParser: @@ -48,7 +47,7 @@ def _get_bone_flags_from_value(value: str) -> int: return 0 -def read_psa_config(psa_sequence_names: List[str], file_path: str) -> PsaConfig: +def read_psa_config(psa_sequence_names: list[str], file_path: str) -> PsaConfig: psa_config = PsaConfig() config = _load_config_file(file_path) diff --git a/io_scene_psk_psa/psa/export/operators.py b/io_scene_psk_psa/psa/export/operators.py index 02a84f9..b80e15a 100644 --- a/io_scene_psk_psa/psa/export/operators.py +++ b/io_scene_psk_psa/psa/export/operators.py @@ -1,6 +1,6 @@ from abc import abstractmethod from collections import Counter -from typing import List, Iterable, Dict, Protocol, Sequence, Tuple, cast as typing_cast +from typing import Iterable, Sequence, Tuple, cast as typing_cast import bpy import re @@ -19,7 +19,7 @@ from .ui import PSA_UL_export_sequences from ..builder import build_psa, PsaBuildSequence, PsaBuildOptions from psk_psa_py.psa.writer import write_psa_to_file from ...shared.helpers import get_collection_export_operator_from_context, get_collection_from_context, get_psk_input_objects_for_collection, populate_bone_collection_list, get_nla_strips_in_frame_range, PsxBoneCollection -from ...shared.types import BpyCollectionProperty, PSX_PG_action_export +from ...shared.types import PSX_PG_action_export from ...shared.ui import draw_bone_filter_mode from ...shared.operators import PSK_OT_bone_collection_list_populate, PSK_OT_bone_collection_list_select_all @@ -179,7 +179,7 @@ def get_sequence_compression_ratio( def get_timeline_marker_sequence_frame_ranges( animation_data: AnimData, scene: Scene, - marker_names: List[str], + marker_names: list[str], ) -> dict[str, tuple[int, int]]: # Timeline markers need to be sorted so that we can determine the sequence start and end positions. sequence_frame_ranges: dict[str, tuple[int, int]] = dict() @@ -240,7 +240,7 @@ def get_sequences_from_action(action: Action): def get_sequences_from_action_pose_markers( action: Action, - pose_markers: List[TimelineMarker], + pose_markers: list[TimelineMarker], pose_marker: TimelineMarker, pose_marker_index: int, ): @@ -259,7 +259,7 @@ def get_sequences_from_action_pose_markers( yield from get_sequences_from_name_and_frame_range(sequence_name, frame_start, frame_end) -def get_visible_sequences(pg: PsaExportMixin, sequences) -> List[PsaExportSequenceMixin]: +def get_visible_sequences(pg: PsaExportMixin, sequences) -> list[PsaExportSequenceMixin]: visible_sequences = [] for i, flag in enumerate(filter_sequences(pg, sequences)): if bool(flag & (1 << 30)): @@ -471,7 +471,7 @@ def create_psa_export_options(context: Context, armature_objects: Sequence[Objec raise RuntimeError(f'No armatures') animation_data = armature_objects[0].animation_data - export_sequences: List[PsaBuildSequence] = [] + export_sequences: list[PsaBuildSequence] = [] # TODO: this needs to be changed so that we iterate over all of the armature objects? # do we need to check for primary key? (data vs. object?) @@ -507,7 +507,7 @@ def create_psa_export_options(context: Context, armature_objects: Sequence[Objec export_sequences.append(export_sequence) case 'TIMELINE_MARKERS': for marker_item in filter(lambda x: x.is_selected, pg.marker_list): - nla_strips_actions: List[Action] = [] + nla_strips_actions: list[Action] = [] for nla_strip in get_nla_strips_in_frame_range(animation_data, marker_item.frame_start, marker_item.frame_end): if nla_strip.action: nla_strips_actions.append(nla_strip.action) @@ -584,7 +584,7 @@ class PSA_OT_export(Operator, ExportHelper): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.armature_objects: List[Object] = [] + self.armature_objects: list[Object] = [] @classmethod def poll(cls, context): diff --git a/io_scene_psk_psa/psa/export/properties.py b/io_scene_psk_psa/psa/export/properties.py index cf1cc9d..5105ec3 100644 --- a/io_scene_psk_psa/psa/export/properties.py +++ b/io_scene_psk_psa/psa/export/properties.py @@ -1,7 +1,7 @@ import re import sys from fnmatch import fnmatch -from typing import List, Optional, Sequence +from typing import Sequence import bpy from bpy.props import ( BoolProperty, @@ -58,7 +58,7 @@ class PSA_PG_export_sequence(PsaExportSequenceMixin): def get_sequences_from_name_and_frame_range(name: str, frame_start: int, frame_end: int): # Check for loop - anims: List[tuple[str, int, int]] = [] + anims: list[tuple[str, int, int]] = [] loop_pattern = r'\@(\d+)\:(.+)' loop_match = re.match(loop_pattern, name) if loop_match: @@ -107,7 +107,7 @@ def nla_track_update_cb(self: 'PSA_PG_export', context: Context) -> None: strip.frame_end = frame_end -def get_animation_data(pg: 'PSA_PG_export', context: Context) -> Optional[AnimData]: +def get_animation_data(pg: 'PSA_PG_export', context: Context) -> AnimData | None: animation_data_object = context.object return animation_data_object.animation_data if animation_data_object else None diff --git a/io_scene_psk_psa/psa/import_/operators.py b/io_scene_psk_psa/psa/import_/operators.py index 70c35ef..84f3976 100644 --- a/io_scene_psk_psa/psa/import_/operators.py +++ b/io_scene_psk_psa/psa/import_/operators.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Iterable from bpy.props import CollectionProperty, StringProperty -from bpy.types import Context, Event, FileHandler, Object, Operator, OperatorFileListElement +from bpy.types import Context, Event, Object, Operator, OperatorFileListElement from bpy_extras.io_utils import ImportHelper from .properties import PsaImportMixin, get_visible_sequences diff --git a/io_scene_psk_psa/psa/import_/properties.py b/io_scene_psk_psa/psa/import_/properties.py index 294fc42..5a465dc 100644 --- a/io_scene_psk_psa/psa/import_/properties.py +++ b/io_scene_psk_psa/psa/import_/properties.py @@ -1,6 +1,5 @@ import re from fnmatch import fnmatch -from typing import List from bpy.props import ( BoolProperty, @@ -133,7 +132,7 @@ class PSA_PG_import(PropertyGroup): select_text: PointerProperty(type=Text) -def filter_sequences(pg: PSA_PG_import, sequences) -> List[int]: +def filter_sequences(pg: PSA_PG_import, sequences) -> list[int]: bitflag_filter_item = 1 << 30 flt_flags = [bitflag_filter_item] * len(sequences) @@ -167,7 +166,7 @@ def filter_sequences(pg: PSA_PG_import, sequences) -> List[int]: return flt_flags -def get_visible_sequences(pg: PSA_PG_import, sequences) -> List[PSA_PG_import_action_list_item]: +def get_visible_sequences(pg: PSA_PG_import, sequences) -> list[PSA_PG_import_action_list_item]: bitflag_filter_item = 1 << 30 visible_sequences = [] for i, flag in enumerate(filter_sequences(pg, sequences)): diff --git a/io_scene_psk_psa/psa/importer.py b/io_scene_psk_psa/psa/importer.py index 8f9fc2a..584a119 100644 --- a/io_scene_psk_psa/psa/importer.py +++ b/io_scene_psk_psa/psa/importer.py @@ -1,4 +1,4 @@ -from typing import Sequence, Iterable, List, Optional, cast as typing_cast +from typing import Sequence, Iterable, cast as typing_cast import bpy import numpy as np @@ -32,7 +32,7 @@ class PsaImportOptions(object): fps_custom: float = 30.0, fps_source: str = 'SEQUENCE', psa_config: PsaConfig = PsaConfig(), - sequence_names: Optional[List[str]] = None, + sequence_names: list[str] | None = None, should_convert_to_samples: bool = False, should_overwrite: bool = False, should_stash: bool = False, @@ -61,13 +61,13 @@ class PsaImportOptions(object): class ImportBone(object): def __init__(self, psa_bone: PsxBone): self.psa_bone: PsxBone = psa_bone - self.parent: Optional[ImportBone] = None - self.armature_bone: Optional[Bone] = None - self.pose_bone: Optional[PoseBone] = None + self.parent: ImportBone | None = None + self.armature_bone: Bone | None = None + self.pose_bone: PoseBone | None = None self.original_location: Vector = Vector() self.original_rotation: Quaternion = Quaternion() self.post_rotation: Quaternion = Quaternion() - self.fcurves: List[FCurve] = [] + self.fcurves: list[FCurve] = [] def _calculate_fcurve_data(import_bone: ImportBone, key_data: Sequence[float]): @@ -90,10 +90,10 @@ def _calculate_fcurve_data(import_bone: ImportBone, key_data: Sequence[float]): class PsaImportResult: def __init__(self): - self.warnings: List[str] = [] + self.warnings: list[str] = [] -def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: List[str], bone_mapping: BoneMapping) -> Optional[int]: +def _get_armature_bone_index_for_psa_bone(psa_bone_name: str, armature_bone_names: list[str], bone_mapping: BoneMapping) -> int | None: """ @param psa_bone_name: The name of the PSA bone. @param armature_bone_names: The names of the bones in the armature. diff --git a/io_scene_psk_psa/psk/builder.py b/io_scene_psk_psa/psk/builder.py index 5e15ca3..6834fc2 100644 --- a/io_scene_psk_psa/psk/builder.py +++ b/io_scene_psk_psa/psk/builder.py @@ -1,9 +1,9 @@ import bmesh import bpy import numpy as np -from bpy.types import Armature, Context, Object, Mesh +from bpy.types import Armature, Context, Object, Mesh, Material from mathutils import Matrix -from typing import Dict, Iterable, List, Optional, cast as typing_cast +from typing import Iterable, Sequence, cast as typing_cast from psk_psa_py.shared.data import Vector3 from psk_psa_py.psk.data import Psk from .properties import triangle_type_and_bit_flags_to_poly_flags @@ -23,10 +23,10 @@ from ..shared.helpers import ( class PskBuildOptions(object): def __init__(self): self.bone_filter_mode = 'ALL' - self.bone_collection_indices: List[PsxBoneCollection] = [] + self.bone_collection_indices: list[PsxBoneCollection] = [] self.object_eval_state = 'EVALUATED' self.material_order_mode = 'AUTOMATIC' - self.material_name_list: List[str] = [] + self.material_name_list: list[str] = [] self.scale = 1.0 self.export_space = 'WORLD' self.forward_axis = 'X' @@ -37,7 +37,7 @@ class PskBuildOptions(object): class PskBuildResult(object): def __init__(self, psk: Psk, warnings: list[str]): self.psk: Psk = psk - self.warnings: List[str] = warnings + self.warnings: list[str] = warnings def _get_mesh_export_space_matrix(node: ObjectNode | None, export_space: str) -> Matrix: @@ -70,7 +70,7 @@ def _get_mesh_export_space_matrix(node: ObjectNode | None, export_space: str) -> assert False, f'Invalid export space: {export_space}' -def _get_material_name_indices(obj: Object, material_names: List[str]) -> Iterable[int]: +def _get_material_name_indices(obj: Object, material_names: list[str]) -> Iterable[int]: """ Returns the index of the material in the list of material names. If the material is not found, the index 0 is returned. @@ -206,7 +206,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil bm.to_mesh(mesh_data) del bm evaluated_mesh_object = bpy.data.objects.new('', mesh_data) - mesh_object = evaluated_mesh_object + mesh_object = evaluated_mesh_object mesh_object.matrix_world = matrix_world # Extract the scale from the matrix. @@ -271,6 +271,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil for loop_index, loop in enumerate(mesh_data.loops): wedges.append(Psk.Wedge(point_index=loop.vertex_index + vertex_offset, u=0.0, v=0.0)) + # Assign material indices to the wedges. for triangle in mesh_data.loop_triangles: for loop_index in triangle.loops: @@ -315,7 +316,7 @@ def build_psk(context: Context, input_objects: PskInputObjects, options: PskBuil bone_names = psx_bone_create_result.armature_object_bone_names[armature_object] vertex_group_names = [x.name for x in mesh_object.vertex_groups] - vertex_group_bone_indices: Dict[int, int] = dict() + vertex_group_bone_indices: dict[int, int] = dict() for vertex_group_index, vertex_group_name in enumerate(vertex_group_names): try: vertex_group_bone_indices[vertex_group_index] = bone_names.index(vertex_group_name) + bone_index_offset diff --git a/io_scene_psk_psa/psk/export/operators.py b/io_scene_psk_psa/psk/export/operators.py index af58a72..873d8d8 100644 --- a/io_scene_psk_psa/psk/export/operators.py +++ b/io_scene_psk_psa/psk/export/operators.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Iterable, List, cast as typing_cast +from typing import Iterable, cast as typing_cast import bpy from bpy.props import StringProperty @@ -173,7 +173,7 @@ class PSK_OT_material_list_name_move_down(Operator): return {'FINISHED'} -def get_sorted_materials_by_names(materials: Iterable[Material], material_names: List[str]) -> List[Material]: +def get_sorted_materials_by_names(materials: Iterable[Material], material_names: list[str]) -> list[Material]: """ Sorts the materials by the order of the material names list. Any materials not in the list will be appended to the end of the list in the order they are found. diff --git a/io_scene_psk_psa/psk/importer.py b/io_scene_psk_psa/psk/importer.py index b9b64ed..ecc10c4 100644 --- a/io_scene_psk_psa/psk/importer.py +++ b/io_scene_psk_psa/psk/importer.py @@ -4,7 +4,7 @@ import numpy as np from bpy.types import Context, Object, VertexGroup, ArmatureModifier, FloatColorAttribute from mathutils import Matrix, Quaternion, Vector -from typing import List, Optional, cast as typing_cast +from typing import cast as typing_cast from psk_psa_py.psk.data import Psk from psk_psa_py.shared.data import PsxBone @@ -35,12 +35,11 @@ class ImportBone: def __init__(self, index: int, psk_bone: PsxBone): self.index: int = index self.psk_bone: PsxBone = psk_bone - self.parent: Optional[ImportBone] = None + self.parent: ImportBone | None = None self.local_rotation: Quaternion = Quaternion() self.local_translation: Vector = Vector() self.world_rotation_matrix: Matrix = Matrix() self.world_matrix: Matrix = Matrix() - self.vertex_group = None self.original_rotation: Quaternion = Quaternion() self.original_location: Vector = Vector() self.post_rotation: Quaternion = Quaternion() @@ -48,9 +47,9 @@ class ImportBone: class PskImportResult: def __init__(self): - self.warnings: List[str] = [] - self.armature_object: Optional[Object] = None - self.mesh_object: Optional[Object] = None + self.warnings: list[str] = [] + self.armature_object: Object | None = None + self.mesh_object: Object | None = None @property def root_object(self) -> Object: @@ -83,7 +82,7 @@ def import_psk(psk: Psk, context: Context, name: str, options: PskImportOptions) bpy.ops.object.mode_set(mode='EDIT') - import_bones: List[ImportBone] = [] + import_bones: list[ImportBone] = [] for bone_index, psk_bone in enumerate(psk.bones): import_bone = ImportBone(bone_index, psk_bone) @@ -263,7 +262,7 @@ def import_psk(psk: Psk, context: Context, name: str, options: PskImportOptions) # Weights # Get a list of all bones that have weights associated with them. vertex_group_bone_indices = set(map(lambda weight: weight.bone_index, psk.weights)) - vertex_groups: List[Optional[VertexGroup]] = [None] * len(psk.bones) + vertex_groups: list[VertexGroup | None] = [None] * len(psk.bones) for bone_index, psk_bone in map(lambda x: (x, psk.bones[x]), vertex_group_bone_indices): vertex_groups[bone_index] = mesh_object.vertex_groups.new(name=psk_bone.name.decode('windows-1252')) diff --git a/io_scene_psk_psa/psk/properties.pyi b/io_scene_psk_psa/psk/properties.pyi index c92bca9..f5b4d2b 100644 --- a/io_scene_psk_psa/psk/properties.pyi +++ b/io_scene_psk_psa/psk/properties.pyi @@ -16,3 +16,8 @@ class PskImportMixin: should_import_shape_keys: bool scale: float bdk_repository_id: str + + +def triangle_type_and_bit_flags_to_poly_flags(mesh_triangle_type: str, mesh_triangle_bit_flags: set[str]) -> int: ... + +def poly_flags_to_triangle_type_and_bit_flags(poly_flags: int) -> tuple[str, set[str]]: ... diff --git a/io_scene_psk_psa/shared/dfs.py b/io_scene_psk_psa/shared/dfs.py index cadc583..4015e9a 100644 --- a/io_scene_psk_psa/shared/dfs.py +++ b/io_scene_psk_psa/shared/dfs.py @@ -5,7 +5,7 @@ These functions are used to iterate over objects in a collection or view layer i instances. This is useful for exporters that need to traverse the object hierarchy in a predictable order. """ -from typing import Optional, Set, Iterable, List +from typing import Iterable from bpy.types import Collection, Object, ViewLayer, LayerCollection from mathutils import Matrix @@ -15,7 +15,7 @@ class DfsObject: """ Represents an object in a depth-first search. """ - def __init__(self, obj: Object, instance_objects: List[Object], matrix_world: Matrix): + def __init__(self, obj: Object, instance_objects: list[Object], matrix_world: Matrix): self.obj = obj self.instance_objects = instance_objects self.matrix_world = matrix_world @@ -85,9 +85,9 @@ def dfs_collection_objects(collection: Collection, visible_only: bool = False) - def _dfs_collection_objects_recursive( collection: Collection, - instance_objects: Optional[List[Object]] = None, + instance_objects: list[Object] | None = None, matrix_world: Matrix = Matrix.Identity(4), - visited: Optional[Set[Object]]=None + visited: set[Object] | None = None ) -> Iterable[DfsObject]: """ Depth-first search of objects in a collection, including recursing into instances. diff --git a/io_scene_psk_psa/shared/helpers.py b/io_scene_psk_psa/shared/helpers.py index 915f810..e90c36c 100644 --- a/io_scene_psk_psa/shared/helpers.py +++ b/io_scene_psk_psa/shared/helpers.py @@ -1,6 +1,6 @@ import bpy from collections import Counter -from typing import List, Iterable, Optional, Dict, Tuple, cast as typing_cast +from typing import Iterable, cast as typing_cast from bpy.types import Armature, AnimData, Collection, Context, Object, ArmatureModifier, SpaceProperties, PropertyGroup from mathutils import Matrix, Vector, Quaternion as BpyQuaternion from psk_psa_py.shared.data import PsxBone, Quaternion, Vector3 @@ -102,7 +102,7 @@ def populate_bone_collection_list( item.is_selected = bone_collection.name in selected_assigned_collection_names if has_selected_collections else True -def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_collection_indices: Iterable[int]) -> List[str]: +def get_export_bone_names(armature_object: Object, bone_filter_mode: str, bone_collection_indices: Iterable[int]) -> list[str]: """ Returns a sorted list of bone indices that should be exported for the given bone filter mode and bone collections. @@ -172,9 +172,9 @@ def convert_string_to_cp1252_bytes(string: str) -> bytes: def create_psx_bones_from_blender_bones( - bones: List[bpy.types.Bone], + bones: list[bpy.types.Bone], armature_object_matrix_world: Matrix, -) -> List[PsxBone]: +) -> list[PsxBone]: """ Creates PSX bones from the given Blender bones. @@ -183,7 +183,7 @@ def create_psx_bones_from_blender_bones( # Apply the scale of the armature object to the bone location. _, _, armature_object_scale = armature_object_matrix_world.decompose() - psx_bones: List[PsxBone] = [] + psx_bones: list[PsxBone] = [] for bone in bones: psx_bone = PsxBone() psx_bone.name = convert_string_to_cp1252_bytes(bone.name) @@ -237,10 +237,6 @@ class PsxBoneCreateResult: self.bones = bones self.armature_object_root_bone_indices = armature_object_root_bone_indices self.armature_object_bone_names = armature_object_bone_names - - @property - def has_false_root_bone(self) -> bool: - return len(self.bones) > 0 and self.bones[0].armature_object is None def convert_vector_to_vector3(vector: Vector) -> Vector3: @@ -280,7 +276,7 @@ class ObjectNode: def __init__(self, obj: Object): self.object = obj self.parent: ObjectNode | None = None - self.children: List[ObjectNode] = [] + self.children: list[ObjectNode] = [] @property def root(self): @@ -298,8 +294,8 @@ class ObjectTree: A tree of the armature objects based on their hierarchy. ''' def __init__(self, objects: Iterable[Object]): - self.root_nodes: List[ObjectNode] = [] - object_node_map: Dict[Object, ObjectNode] = {x: ObjectNode(x) for x in objects} + self.root_nodes: list[ObjectNode] = [] + object_node_map: dict[Object, ObjectNode] = {x: ObjectNode(x) for x in objects} for obj, object_node in object_node_map.items(): if obj.parent in object_node_map: @@ -334,14 +330,14 @@ class ObjectTree: def create_psx_bones( - armature_objects: List[Object], + armature_objects: list[Object], export_space: str = 'WORLD', root_bone_name: str = 'ROOT', forward_axis: str = 'X', up_axis: str = 'Z', scale: float = 1.0, bone_filter_mode: str = 'ALL', - bone_collection_indices: Optional[List[PsxBoneCollection]] = None, + bone_collection_indices: list[PsxBoneCollection] | None = None, bone_collection_primary_key: str = 'OBJECT', ) -> PsxBoneCreateResult: """ @@ -366,9 +362,9 @@ def create_psx_bones( total_bone_count += len(armature_data.bones) # Store the bone names to be exported for each armature object. - armature_object_bone_names: Dict[Object, List[str]] = dict() + armature_object_bone_names: dict[Object, list[str]] = dict() for armature_object in armature_objects: - armature_bone_collection_indices: List[int] = [] + armature_bone_collection_indices: list[int] = [] match bone_collection_primary_key: case 'OBJECT': armature_bone_collection_indices.extend([x.index for x in bone_collection_indices if x.armature_object_name == armature_object.name]) @@ -390,6 +386,13 @@ def create_psx_bones( armature_data = typing_cast(Armature, armature_object.data) armature_bones = [armature_data.bones[bone_name] for bone_name in bone_names] + # Ensure that we don't have multiple root bones in this armature. + root_bone_count = sum(1 for bone in armature_bones if bone.parent is None) + if root_bone_count > 1: + raise RuntimeError(f'Armature object \'{armature_object.name}\' has multiple root bones. ' + f'Only one root bone is allowed per armature.' + ) + armature_psx_bones = create_psx_bones_from_blender_bones( bones=armature_bones, armature_object_matrix_world=armature_object.matrix_world, @@ -611,8 +614,8 @@ from bpy.types import Depsgraph class PskInputObjects(object): def __init__(self): - self.mesh_dfs_objects: List[DfsObject] = [] - self.armature_objects: List[Object] = [] + self.mesh_dfs_objects: list[DfsObject] = [] + self.armature_objects: list[Object] = [] def get_materials_for_mesh_objects(depsgraph: Depsgraph, mesh_objects: Iterable[Object]): @@ -640,7 +643,7 @@ def get_mesh_objects_for_context(context: Context) -> Iterable[DfsObject]: yield dfs_object -def get_armature_for_mesh_object(mesh_object: Object) -> Optional[Object]: +def get_armature_for_mesh_object(mesh_object: Object) -> Object | None: if mesh_object.type != 'MESH': return None # Get the first armature modifier with a non-empty armature object. diff --git a/io_scene_psk_psa/shared/types.pyi b/io_scene_psk_psa/shared/types.pyi index 735280a..ddf8735 100644 --- a/io_scene_psk_psa/shared/types.pyi +++ b/io_scene_psk_psa/shared/types.pyi @@ -59,3 +59,5 @@ class PsxBoneExportMixin: class PSX_PG_scene_export(TransformSourceMixin): pass + +bone_filter_mode_items: tuple[tuple[str, str, str]]