Files
blender-vrm/editor/spring_bone1/handler.py
小煜 091ad6a49a feat: Add VRM Blender addon with complete import/export functionality
- Add core VRM addon infrastructure with manifest and registration
- Add common utilities module with file system, logging, and conversion helpers
- Add human bone mapper with support for multiple rigging standards (Mixamo, MMD, Unreal, Rigify, etc.)
- Add VRM 0.x and 1.x format support with property groups and handlers
- Add editor UI panels for VRM metadata, spring bones, and MToon materials
- Add exporter with glTF2 extension support for VRM format serialization
- Add importer with scene reconstruction and armature generation
- Add MToon shader support with auto-setup and material migration
- Add spring bone physics simulation with constraint handling
- Add node constraint editor for advanced rigging control
- Add comprehensive validation and error handling with user dialogs
- Add scene watcher for real-time property synchronization
- Add workspace management and preference system
- Include Python cache files and Blender manifest configuration
- This is the initial commit establishing the complete VRM addon ecosystem for Blender
2026-01-01 14:21:56 +08:00

859 lines
31 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
from collections.abc import Iterable, Sequence
from dataclasses import dataclass
from decimal import Decimal
from sys import float_info
from typing import Optional, Union
import bpy
from bpy.app.handlers import persistent
from bpy.types import Armature, Context, Object, PoseBone
from mathutils import Matrix, Quaternion, Vector
from ...common.rotation import (
get_rotation_as_quaternion,
set_rotation_without_mode_change,
)
from ..extension import get_armature_extension
from ..property_group import CollectionPropertyProtocol
from .property_group import (
SpringBone1JointPropertyGroup,
SpringBone1SpringPropertyGroup,
)
@dataclass
class State:
frame_count: Decimal = Decimal()
spring_bone_60_fps_update_count: Decimal = Decimal()
last_fps: Optional[Decimal] = None
last_fps_base: Optional[Decimal] = None
def reset(self, context: Context) -> None:
self.frame_count = Decimal()
self.spring_bone_60_fps_update_count = Decimal()
self.last_fps_base = Decimal(context.scene.render.fps_base)
self.last_fps = Decimal(context.scene.render.fps)
state = State()
def reset_state(context: Context) -> None:
state.reset(context)
@dataclass(frozen=True)
class SphereWorldCollider:
offset: Vector
radius: float
def calculate_collision(
self, target: Vector, target_radius: float
) -> tuple[Vector, float]:
diff = target - self.offset
diff_length = diff.length
if diff_length < float_info.epsilon:
return Vector((0, 0, -1)), -0.01
return diff / diff_length, diff_length - target_radius - self.radius
@dataclass(frozen=True)
class CapsuleWorldCollider:
offset: Vector
radius: float
tail: Vector
offset_to_tail_diff: Vector # Must be non-zero vector
offset_to_tail_diff_length_squared: float # Must be non-negative value
def calculate_collision(
self, target: Vector, target_radius: float
) -> tuple[Vector, float]:
offset_to_target_diff = target - self.offset
# Find the shortest point on the line containing offset and tail to the target
# self.offset + (self.tail - self.offset) * offset_to_tail_ratio_for_nearest
# Calculate offset_to_tail_ratio_for_nearest to express it as the above formula
offset_to_tail_ratio_for_nearest = (
self.offset_to_tail_diff.dot(offset_to_target_diff)
/ self.offset_to_tail_diff_length_squared
)
# The line segment from offset to tail has start point 0 and end point 1,
# so clamp outside ranges
offset_to_tail_ratio_for_nearest = max(
0, min(1, offset_to_tail_ratio_for_nearest)
)
# Calculate the shortest point to the target
nearest = (
self.offset + self.offset_to_tail_diff * offset_to_tail_ratio_for_nearest
)
# Collision detection
diff = target - nearest
diff_length = diff.length
if diff_length < float_info.epsilon:
return Vector((0, 0, -1)), -0.01
return diff / diff_length, diff_length - target_radius - self.radius
@dataclass(frozen=True)
class SphereInsideWorldCollider:
offset: Vector
radius: float
def calculate_collision(
self, target: Vector, target_radius: float
) -> tuple[Vector, float]:
diff = self.offset - target
diff_length = diff.length
if diff_length < float_info.epsilon:
return Vector((0, 0, -1)), -0.01
return diff / diff_length, -diff_length - target_radius + self.radius
@dataclass(frozen=True)
class CapsuleInsideWorldCollider:
offset: Vector
radius: float
tail: Vector
offset_to_tail_diff: Vector # Must be non-zero vector
offset_to_tail_diff_length_squared: float # Must be non-negative value
def calculate_collision(
self, target: Vector, target_radius: float
) -> tuple[Vector, float]:
offset_to_target_diff = target - self.offset
# Find the shortest point on the line containing offset and tail to the target
# self.offset + (self.tail - self.offset) * offset_to_tail_ratio_for_nearest
# Calculate offset_to_tail_ratio_for_nearest to express it as the above formula
offset_to_tail_ratio_for_nearest = (
self.offset_to_tail_diff.dot(offset_to_target_diff)
/ self.offset_to_tail_diff_length_squared
)
# The line segment from offset to tail has start point 0 and end point 1,
# so clamp outside ranges
offset_to_tail_ratio_for_nearest = max(
0, min(1, offset_to_tail_ratio_for_nearest)
)
# Calculate the shortest point to the target
nearest = (
self.offset + self.offset_to_tail_diff * offset_to_tail_ratio_for_nearest
)
# Collision detection
diff = nearest - target
diff_length = diff.length
if diff_length < float_info.epsilon:
return Vector((0, 0, -1)), -0.01
return diff / diff_length, -diff_length - target_radius + self.radius
@dataclass(frozen=True)
class PlaneWorldCollider:
offset: Vector
normal: Vector
def calculate_collision(
self, target: Vector, target_radius: float
) -> tuple[Vector, float]:
distance = (target - self.offset).dot(self.normal) - target_radius
return self.normal, distance
# https://github.com/vrm-c/vrm-specification/tree/993a90a5bda9025f3d9e2923ad6dea7506f88553/specification/VRMC_springBone-1.0#update-procedure
def update_pose_bone_rotations(context: Context, delta_time: float) -> None:
pose_bone_and_rotations: list[tuple[PoseBone, Quaternion]] = []
for obj in context.blend_data.objects:
calculate_object_pose_bone_rotations(delta_time, obj, pose_bone_and_rotations)
for pose_bone, pose_bone_rotation in pose_bone_and_rotations:
# Assigning rotation to pose_bone is expensive, so avoid it as much as possible
angle_diff = pose_bone_rotation.rotation_difference(
get_rotation_as_quaternion(pose_bone)
).angle
if abs(angle_diff) < float_info.epsilon:
continue
set_rotation_without_mode_change(pose_bone, pose_bone_rotation)
def calculate_object_pose_bone_rotations(
delta_time: float,
obj: Object,
pose_bone_and_rotations: list[tuple[PoseBone, Quaternion]],
) -> None:
if obj.type != "ARMATURE":
return
armature_data = obj.data
if not isinstance(armature_data, Armature):
return
ext = get_armature_extension(armature_data)
if not ext.is_vrm1():
return
spring_bone1 = ext.spring_bone1
if not spring_bone1.enable_animation:
return
obj_matrix_world = obj.matrix_world
obj_matrix_world_inverted = obj_matrix_world.inverted_safe()
obj_matrix_world_quaternion = obj_matrix_world.to_quaternion()
collider_uuid_to_world_collider: dict[
str,
Union[
SphereWorldCollider,
CapsuleWorldCollider,
SphereInsideWorldCollider,
CapsuleInsideWorldCollider,
PlaneWorldCollider,
],
] = {}
for collider in spring_bone1.colliders:
pose_bone = obj.pose.bones.get(collider.node.bone_name)
if not pose_bone:
continue
pose_bone_world_matrix = obj_matrix_world @ pose_bone.matrix
extended_collider = collider.extensions.vrmc_spring_bone_extended_collider
world_collider: Union[
None,
SphereWorldCollider,
CapsuleWorldCollider,
SphereInsideWorldCollider,
CapsuleInsideWorldCollider,
PlaneWorldCollider,
] = None
if extended_collider.enabled:
if (
extended_collider.shape_type
== extended_collider.SHAPE_TYPE_EXTENDED_SPHERE.identifier
):
offset = pose_bone_world_matrix @ Vector(
extended_collider.shape.sphere.offset
)
radius = extended_collider.shape.sphere.radius
if extended_collider.shape.sphere.inside:
world_collider = SphereInsideWorldCollider(
offset=offset, radius=radius
)
else:
world_collider = SphereWorldCollider(offset=offset, radius=radius)
elif (
extended_collider.shape_type
== extended_collider.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
):
offset = pose_bone_world_matrix @ Vector(
extended_collider.shape.capsule.offset
)
tail = pose_bone_world_matrix @ Vector(
extended_collider.shape.capsule.tail
)
radius = extended_collider.shape.sphere.radius
offset_to_tail_diff = tail - offset
offset_to_tail_diff_length_squared = offset_to_tail_diff.length_squared
if offset_to_tail_diff_length_squared < float_info.epsilon:
# If offset and tail positions are the same, use as sphere collider
if extended_collider.shape.capsule.inside:
world_collider = SphereInsideWorldCollider(
offset=offset, radius=radius
)
else:
world_collider = SphereWorldCollider(
offset=offset, radius=radius
)
elif extended_collider.shape.capsule.inside:
world_collider = CapsuleInsideWorldCollider(
offset=offset,
radius=radius,
tail=tail,
offset_to_tail_diff=offset_to_tail_diff,
offset_to_tail_diff_length_squared=offset_to_tail_diff_length_squared,
)
else:
world_collider = CapsuleWorldCollider(
offset=offset,
radius=radius,
tail=tail,
offset_to_tail_diff=offset_to_tail_diff,
offset_to_tail_diff_length_squared=offset_to_tail_diff_length_squared,
)
elif (
extended_collider.shape_type
== extended_collider.SHAPE_TYPE_EXTENDED_PLANE.identifier
):
offset = pose_bone_world_matrix @ Vector(
extended_collider.shape.plane.offset
)
normal = pose_bone_world_matrix.to_quaternion() @ Vector(
extended_collider.shape.plane.normal
)
world_collider = PlaneWorldCollider(
offset=offset,
normal=normal,
)
elif collider.shape_type == collider.SHAPE_TYPE_SPHERE.identifier:
offset = pose_bone_world_matrix @ Vector(collider.shape.sphere.offset)
radius = collider.shape.sphere.radius
world_collider = SphereWorldCollider(
offset=offset,
radius=radius,
)
elif collider.shape_type == collider.SHAPE_TYPE_CAPSULE.identifier:
offset = pose_bone_world_matrix @ Vector(collider.shape.capsule.offset)
tail = pose_bone_world_matrix @ Vector(collider.shape.capsule.tail)
radius = collider.shape.sphere.radius
offset_to_tail_diff = tail - offset
offset_to_tail_diff_length_squared = offset_to_tail_diff.length_squared
if offset_to_tail_diff_length_squared < float_info.epsilon:
# If offset and tail positions are the same, use as sphere collider
world_collider = SphereWorldCollider(
offset=offset,
radius=radius,
)
else:
world_collider = CapsuleWorldCollider(
offset=offset,
radius=radius,
tail=tail,
offset_to_tail_diff=offset_to_tail_diff,
offset_to_tail_diff_length_squared=offset_to_tail_diff_length_squared,
)
if world_collider:
collider_uuid_to_world_collider[collider.uuid] = world_collider
collider_group_uuid_to_world_colliders: dict[
str,
list[
Union[
SphereWorldCollider,
CapsuleWorldCollider,
SphereInsideWorldCollider,
CapsuleInsideWorldCollider,
PlaneWorldCollider,
]
],
] = {}
for collider_group in spring_bone1.collider_groups:
for collider_reference in collider_group.colliders:
world_collider = collider_uuid_to_world_collider.get(
collider_reference.collider_uuid
)
if world_collider is None:
continue
world_colliders = collider_group_uuid_to_world_colliders.get(
collider_group.uuid
)
if world_colliders is None:
world_colliders = []
collider_group_uuid_to_world_colliders[collider_group.uuid] = (
world_colliders
)
world_colliders.append(world_collider)
for spring in spring_bone1.springs:
joints = spring.joints
if not joints:
continue
calculate_spring_pose_bone_rotations(
delta_time,
obj,
obj_matrix_world,
obj_matrix_world_inverted,
obj_matrix_world_quaternion,
spring,
pose_bone_and_rotations,
collider_group_uuid_to_world_colliders,
)
def calculate_spring_pose_bone_rotations(
delta_time: float,
obj: Object,
obj_matrix_world: Matrix,
obj_matrix_world_inverted: Matrix,
obj_matrix_world_quaternion: Quaternion,
spring: SpringBone1SpringPropertyGroup,
pose_bone_and_rotations: list[tuple[PoseBone, Quaternion]],
collider_group_uuid_to_world_colliders: dict[
str,
list[
Union[
SphereWorldCollider,
CapsuleWorldCollider,
SphereInsideWorldCollider,
CapsuleInsideWorldCollider,
PlaneWorldCollider,
]
],
],
) -> None:
world_collider_groups: Sequence[
Sequence[
Union[
SphereWorldCollider,
CapsuleWorldCollider,
SphereInsideWorldCollider,
CapsuleInsideWorldCollider,
PlaneWorldCollider,
]
]
] = [
collider_group_world_colliders
for collider_group_reference in spring.collider_groups
if (
collider_group_world_colliders
:= collider_group_uuid_to_world_colliders.get(
collider_group_reference.collider_group_uuid
)
)
and collider_group_world_colliders
]
center_pose_bone = obj.pose.bones.get(spring.center.bone_name)
if center_pose_bone:
current_center_world_translation = (
obj_matrix_world @ center_pose_bone.matrix
).to_translation()
previous_center_world_translation = Vector(
spring.animation_state.previous_center_world_translation
)
previous_to_current_center_world_translation = (
current_center_world_translation - previous_center_world_translation
)
if not spring.animation_state.use_center_space:
spring.animation_state.previous_center_world_translation = (
current_center_world_translation.copy()
)
spring.animation_state.use_center_space = True
else:
current_center_world_translation = Vector((0, 0, 0))
previous_to_current_center_world_translation = Vector((0, 0, 0))
if spring.animation_state.use_center_space:
spring.animation_state.use_center_space = False
for sorted_joint_and_bones in sort_spring_bone_joints(obj, spring.joints):
joints: list[
tuple[
SpringBone1JointPropertyGroup,
PoseBone,
Matrix,
]
] = [
(
joint,
pose_bone,
pose_bone.bone.convert_local_to_pose(
Matrix(), pose_bone.bone.matrix_local
),
)
for joint, pose_bone in sorted_joint_and_bones
]
# https://github.com/vrm-c/vrm-specification/blob/7279e169ac0dcf37e7d81b2adcad9107101d7e25/specification/VRMC_springBone-1.0/README.md#center-space
enable_center_space = False
if center_pose_bone:
first_pose_bone = next((pose_bone for (_, pose_bone, _) in joints), None)
ancestor_of_first_pose_bone: Optional[PoseBone] = first_pose_bone
while ancestor_of_first_pose_bone:
if center_pose_bone == ancestor_of_first_pose_bone:
enable_center_space = True
break
ancestor_of_first_pose_bone = ancestor_of_first_pose_bone.parent
next_head_pose_bone_before_rotation_matrix = None
for (
head_joint,
head_pose_bone,
head_rest_object_matrix,
), (
tail_joint,
tail_pose_bone,
tail_rest_object_matrix,
) in zip(joints, joints[1:]):
head_tail_parented = False
searching_tail_parent = tail_pose_bone.parent
while searching_tail_parent:
if searching_tail_parent.name == head_pose_bone.name:
head_tail_parented = True
break
searching_tail_parent = searching_tail_parent.parent
if not head_tail_parented:
break
(
head_pose_bone_rotation,
next_head_pose_bone_before_rotation_matrix,
) = calculate_joint_pair_head_pose_bone_rotations(
delta_time,
obj_matrix_world,
obj_matrix_world_inverted,
obj_matrix_world_quaternion,
head_joint,
head_pose_bone,
head_rest_object_matrix,
tail_joint,
tail_pose_bone,
tail_rest_object_matrix,
next_head_pose_bone_before_rotation_matrix,
world_collider_groups,
previous_to_current_center_world_translation
if enable_center_space
else Vector((0, 0, 0)),
)
pose_bone_and_rotations.append((head_pose_bone, head_pose_bone_rotation))
spring.animation_state.previous_center_world_translation = (
current_center_world_translation
)
def calculate_joint_pair_head_pose_bone_rotations(
delta_time: float,
obj_matrix_world: Matrix,
obj_matrix_world_inverted: Matrix,
obj_matrix_world_quaternion: Quaternion,
head_joint: SpringBone1JointPropertyGroup,
head_pose_bone: PoseBone,
current_head_rest_object_matrix: Matrix,
tail_joint: SpringBone1JointPropertyGroup,
tail_pose_bone: PoseBone,
current_tail_rest_object_matrix: Matrix,
next_head_pose_bone_before_rotation_matrix: Optional[Matrix],
world_collider_groups: Sequence[
Sequence[
Union[
SphereWorldCollider,
CapsuleWorldCollider,
SphereInsideWorldCollider,
CapsuleInsideWorldCollider,
PlaneWorldCollider,
]
]
],
previous_to_current_center_world_translation: Vector,
) -> tuple[Quaternion, Matrix]:
current_head_pose_bone_matrix = head_pose_bone.matrix
current_tail_pose_bone_matrix = tail_pose_bone.matrix
if next_head_pose_bone_before_rotation_matrix is None:
if head_pose_bone_parent := head_pose_bone.parent:
current_head_parent_matrix = head_pose_bone_parent.matrix
current_head_parent_rest_object_matrix = (
head_pose_bone_parent.bone.convert_local_to_pose(
Matrix(), head_pose_bone_parent.bone.matrix_local
)
)
next_head_pose_bone_before_rotation_matrix = current_head_parent_matrix @ (
current_head_parent_rest_object_matrix.inverted_safe()
@ current_head_rest_object_matrix
)
else:
next_head_pose_bone_before_rotation_matrix = (
current_head_rest_object_matrix.copy()
)
(
next_head_pose_bone_translation,
next_head_parent_pose_bone_object_rotation,
next_head_pose_bone_scale,
) = next_head_pose_bone_before_rotation_matrix.decompose()
next_head_world_translation = obj_matrix_world @ next_head_pose_bone_translation
if not tail_joint.animation_state.initialized_as_tail:
initial_tail_world_translation = (
obj_matrix_world @ current_tail_pose_bone_matrix
).to_translation()
tail_joint.animation_state.initialized_as_tail = True
tail_joint.animation_state.previous_world_translation = list(
initial_tail_world_translation
)
tail_joint.animation_state.current_world_translation = list(
initial_tail_world_translation
)
previous_tail_world_translation = (
Vector(tail_joint.animation_state.previous_world_translation)
+ previous_to_current_center_world_translation
)
current_tail_world_translation = (
Vector(tail_joint.animation_state.current_world_translation)
+ previous_to_current_center_world_translation
)
inertia = (current_tail_world_translation - previous_tail_world_translation) * (
1.0 - head_joint.drag_force
)
current_head_rest_object_matrix_inverted = (
current_head_rest_object_matrix.inverted_safe()
)
next_head_rotation_start_target_local_translation = (
current_head_rest_object_matrix_inverted
@ current_tail_rest_object_matrix.to_translation()
)
stiffness_direction = (
obj_matrix_world_quaternion
@ next_head_parent_pose_bone_object_rotation
@ next_head_rotation_start_target_local_translation
).normalized()
stiffness = stiffness_direction * delta_time * head_joint.stiffness
external = Vector(head_joint.gravity_dir) * delta_time * head_joint.gravity_power
next_tail_world_translation = (
current_tail_world_translation + inertia + stiffness + external
)
head_to_tail_world_distance = (
obj_matrix_world @ current_head_pose_bone_matrix.to_translation()
- (obj_matrix_world @ current_tail_pose_bone_matrix.to_translation())
).length
# Apply distance constraint to next Tail
next_tail_world_translation = (
next_head_world_translation
+ (next_tail_world_translation - next_head_world_translation).normalized()
* head_to_tail_world_distance
)
# Calculate collider collision
for world_colliders in world_collider_groups:
for world_collider in world_colliders:
direction, distance = world_collider.calculate_collision(
next_tail_world_translation,
head_joint.hit_radius,
)
if distance >= 0:
continue
# Push away
next_tail_world_translation = (
next_tail_world_translation - direction * distance
)
# Apply distance constraint to next Tail
next_tail_world_translation = (
next_head_world_translation
+ (
next_tail_world_translation - next_head_world_translation
).normalized()
* head_to_tail_world_distance
)
next_tail_object_local_translation = (
obj_matrix_world_inverted @ next_tail_world_translation
)
next_head_rotation_end_target_local_translation = (
next_head_pose_bone_before_rotation_matrix.inverted_safe()
@ next_tail_object_local_translation
)
next_head_pose_bone_rotation = Quaternion(
next_head_rotation_start_target_local_translation.cross(
next_head_rotation_end_target_local_translation
),
next_head_rotation_start_target_local_translation.angle(
next_head_rotation_end_target_local_translation, 0
),
)
next_head_pose_bone_object_rotation = (
next_head_parent_pose_bone_object_rotation @ next_head_pose_bone_rotation
)
next_head_pose_bone_matrix = (
Matrix.Translation(next_head_pose_bone_translation)
@ next_head_pose_bone_object_rotation.to_matrix().to_4x4()
@ Matrix.Diagonal(next_head_pose_bone_scale).to_4x4()
)
next_tail_pose_bone_before_rotation_matrix = (
next_head_pose_bone_matrix
@ current_head_rest_object_matrix_inverted
@ current_tail_rest_object_matrix
)
tail_joint.animation_state.previous_world_translation = list(
current_tail_world_translation
)
tail_joint.animation_state.current_world_translation = list(
next_tail_world_translation
)
return (
next_head_pose_bone_rotation
if head_pose_bone.bone.use_inherit_rotation
else next_head_pose_bone_object_rotation,
next_tail_pose_bone_before_rotation_matrix,
)
@persistent
def depsgraph_update_pre(_unused: object) -> None:
context = bpy.context
state.reset(context)
@persistent
def frame_change_pre(_unused: object) -> None:
context = bpy.context
fps = Decimal(context.scene.render.fps)
last_fps = state.last_fps
fps_base = Decimal(context.scene.render.fps_base)
last_fps_base = state.last_fps_base
if (
last_fps_base is None
or (fps_base - last_fps_base).copy_abs() > 0.00001
or fps != last_fps
):
state.reset(context)
state.frame_count += 1
# If the current time is future than the next SpringBone calculation
# time, move the SpringBone
# To minimize floating-point rounding errors, multiply numerator by
# common denominator to minimize decimal handling
frame_time_x_60_x_fps = state.frame_count * Decimal(60) * fps_base
while True:
next_spring_bone_60_fps_update_count = (
state.spring_bone_60_fps_update_count + Decimal(1)
)
next_spring_bone_update_time_x_60_x_fps = (
next_spring_bone_60_fps_update_count * fps
)
if next_spring_bone_update_time_x_60_x_fps > frame_time_x_60_x_fps:
break
# To accumulate float rounding errors, don't hardcode delta_time as 1.0/60.0
# Use the difference between previous and next times
next_spring_bone_update_time = next_spring_bone_60_fps_update_count / Decimal(
60
)
current_spring_bone_update_time = (
state.spring_bone_60_fps_update_count / Decimal(60)
)
delta_time = float(next_spring_bone_update_time) - float(
current_spring_bone_update_time
)
update_pose_bone_rotations(context, delta_time)
state.spring_bone_60_fps_update_count += 1
def sort_spring_bone_joints(
obj: Object, joints: CollectionPropertyProtocol[SpringBone1JointPropertyGroup]
) -> Sequence[Iterable[tuple[SpringBone1JointPropertyGroup, PoseBone]]]:
bones = obj.pose.bones
# Check if it's sorted and return as-is if already sorted.
# This is logically unnecessary but done for simulation efficiency.
already_sorted = True
sorted_pose_bones: list[PoseBone] = []
for joint in joints:
joint_bone = bones.get(joint.node.bone_name)
if not joint_bone:
already_sorted = False
break
if not sorted_pose_bones:
sorted_pose_bones.append(joint_bone)
continue
parent_bone = sorted_pose_bones[-1]
sorted_pose_bones.append(joint_bone)
traversing_bone = joint_bone.parent
connected = False
while traversing_bone:
if traversing_bone == parent_bone:
connected = True
break
traversing_bone = traversing_bone.parent
if not connected:
already_sorted = False
break
if already_sorted:
return [zip(joints, sorted_pose_bones)]
# Perform sorting
chains = list[list[tuple[SpringBone1JointPropertyGroup, PoseBone]]]()
for joint in joints:
joint_bone = bones.get(joint.node.bone_name)
if not joint_bone:
continue
if not chains:
chains.append([(joint, joint_bone)])
continue
# Skip if already registered in chain
if any(joint_bone == bone for chain in chains for _, bone in chain):
continue
# If ancestor of chain head, or descendant of chain tail,
# or descendant of chain head and ancestor of chain tail,
# add to that chain
# Otherwise, create a new chain
assigned = False
for chain in chains:
if not chain:
# This should not happen
continue
# Check if it's an ancestor of the chain head
_, chain_head_bone = chain[0]
traversing_bone = chain_head_bone.parent
assigned = False
while traversing_bone:
if traversing_bone == joint_bone:
chain.insert(0, (joint, joint_bone))
assigned = True
break
traversing_bone = traversing_bone.parent
if assigned:
break
# Check if it's an ancestor of the chain tail
_, chain_tail_bone = chain[-1]
traversing_bone = joint_bone.parent
assigned = False
while traversing_bone:
if traversing_bone == chain_tail_bone:
chain.append((joint, joint_bone))
assigned = True
break
traversing_bone = traversing_bone.parent
if assigned:
break
# Check if it's a descendant of the chain head and ancestor of
# the chain tail
assigned = False
for i in range(len(chain) - 1):
_, chain_parent_bone = chain[i]
_, chain_child_bone = chain[i + 1]
traversing_bone = chain_child_bone.parent
while traversing_bone:
if traversing_bone == joint_bone:
chain.insert(i + 1, (joint, joint_bone))
assigned = True
break
if traversing_bone == chain_parent_bone:
break
traversing_bone = traversing_bone.parent
if assigned:
break
if assigned:
break
if not assigned:
chains.append([(joint, joint_bone)])
return chains