2023-12-29 12:31:34 +01:00
|
|
|
# SPDX-FileCopyrightText: 2023 Blender Authors
|
|
|
|
|
#
|
|
|
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
|
|
|
|
|
|
"""
|
2024-02-14 00:13:38 +11:00
|
|
|
blender -b --factory-startup --python tests/python/bl_animation_armature.py
|
2023-12-29 12:31:34 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import unittest
|
2025-10-13 12:12:25 +02:00
|
|
|
from typing import TypeAlias
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
import bpy
|
2025-10-13 12:12:25 +02:00
|
|
|
from mathutils import Vector
|
|
|
|
|
from bpy.types import EditBone, Object, Armature
|
|
|
|
|
|
|
|
|
|
Vectorish: TypeAlias = Vector | tuple[float, float, float]
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class BoneCollectionTest(unittest.TestCase):
|
|
|
|
|
arm_ob: bpy.types.Object
|
|
|
|
|
arm: bpy.types.Armature
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
2024-01-02 15:38:24 +01:00
|
|
|
bpy.ops.wm.read_homefile(use_factory_startup=True)
|
2023-12-29 12:31:34 +01:00
|
|
|
self.arm_ob, self.arm = self.create_armature()
|
|
|
|
|
|
|
|
|
|
def create_armature(self) -> tuple[bpy.types.Object, bpy.types.Armature]:
|
|
|
|
|
arm = bpy.data.armatures.new('Armature')
|
|
|
|
|
arm_ob = bpy.data.objects.new('ArmObject', arm)
|
|
|
|
|
|
|
|
|
|
# Link to the scene just for giggles. And ease of debugging when things
|
|
|
|
|
# go bad.
|
|
|
|
|
bpy.context.scene.collection.objects.link(arm_ob)
|
|
|
|
|
|
|
|
|
|
return arm_ob, arm
|
|
|
|
|
|
2024-01-02 15:40:30 +01:00
|
|
|
def add_bones(self, arm_ob: bpy.types.Object) -> dict[str, bpy.types.Bone]:
|
|
|
|
|
"""Add some test bones to the armature."""
|
2023-12-29 12:31:34 +01:00
|
|
|
|
2024-01-02 15:40:30 +01:00
|
|
|
# Switch to edit mode to add some bones.
|
2023-12-29 12:31:34 +01:00
|
|
|
bpy.context.view_layer.objects.active = arm_ob
|
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
2024-01-02 15:40:30 +01:00
|
|
|
self.assertEqual('EDIT', arm_ob.mode, 'Armature should be in edit mode now')
|
2023-12-29 12:31:34 +01:00
|
|
|
|
2024-01-02 15:40:30 +01:00
|
|
|
arm = arm_ob.data
|
|
|
|
|
bone_names = ('root', 'child_L', 'child_R', 'child_L_L', 'child_L_R', 'child_R_L', 'child_R_R')
|
2023-12-29 12:31:34 +01:00
|
|
|
try:
|
2024-01-02 15:40:30 +01:00
|
|
|
for bone_name in bone_names:
|
|
|
|
|
ebone = arm.edit_bones.new(name=bone_name)
|
|
|
|
|
# Bones have to have a length, or they will be removed when exiting edit mode.
|
|
|
|
|
ebone.tail = (1, 0, 0)
|
|
|
|
|
|
|
|
|
|
arm.edit_bones['child_L'].parent = arm.edit_bones['root']
|
|
|
|
|
arm.edit_bones['child_R'].parent = arm.edit_bones['root']
|
|
|
|
|
arm.edit_bones['child_L_L'].parent = arm.edit_bones['child_L']
|
|
|
|
|
arm.edit_bones['child_L_R'].parent = arm.edit_bones['child_L']
|
|
|
|
|
arm.edit_bones['child_R_L'].parent = arm.edit_bones['child_R']
|
|
|
|
|
arm.edit_bones['child_R_R'].parent = arm.edit_bones['child_R']
|
|
|
|
|
|
2023-12-29 12:31:34 +01:00
|
|
|
finally:
|
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
|
|
2024-01-02 15:40:30 +01:00
|
|
|
# Return the bones, not the editbones.
|
|
|
|
|
return {bone_name: arm.bones[bone_name]
|
|
|
|
|
for bone_name in bone_names}
|
|
|
|
|
|
2023-12-29 12:31:34 +01:00
|
|
|
def test_bone_collection_api(self):
|
|
|
|
|
# Just to keep the rest of the code shorter.
|
|
|
|
|
bcolls = self.arm.collections
|
2024-01-11 14:36:59 +01:00
|
|
|
bcolls_all = self.arm.collections_all
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
self.assertEqual([], list(bcolls), "By default an Armature should have no collections")
|
|
|
|
|
|
|
|
|
|
# Build a hierarchy.
|
|
|
|
|
root1 = bcolls.new('root1')
|
|
|
|
|
r1_child1 = bcolls.new('r1_child1', parent=root1)
|
|
|
|
|
r1_child1_001 = bcolls.new('r1_child1', parent=root1)
|
|
|
|
|
root2 = bcolls.new('root2')
|
|
|
|
|
r2_child1 = bcolls.new('r2_child1', parent=root2)
|
|
|
|
|
r2_child2 = bcolls.new('r2_child2', parent=root2)
|
|
|
|
|
|
|
|
|
|
self.assertEqual('r1_child1.001', r1_child1_001.name, 'Names should be unique')
|
|
|
|
|
|
|
|
|
|
# Check the hierarchy.
|
|
|
|
|
self.assertEqual([root1, root2], list(bcolls), 'armature.collections should reflect only the roots')
|
|
|
|
|
self.assertEqual([r1_child1, r1_child1_001], list(root1.children), 'root1.children should have its children')
|
|
|
|
|
self.assertEqual([r2_child1, r2_child2], list(root2.children), 'root2.children should have its children')
|
|
|
|
|
self.assertEqual([], list(r1_child1.children))
|
2024-01-02 12:47:35 +01:00
|
|
|
self.assertIsNone(root1.parent)
|
|
|
|
|
self.assertEqual(root1, r1_child1.parent)
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
# Check the array order.
|
2024-01-11 14:36:59 +01:00
|
|
|
self.assertEqual([root1, root2, r1_child1, r1_child1_001, r2_child1, r2_child2], list(bcolls_all))
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
# Move root2 to become the child of r1_child1.
|
2024-01-05 16:51:26 +01:00
|
|
|
root2.parent = r1_child1
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
# Check the hierarchy.
|
|
|
|
|
self.assertEqual([root1], list(bcolls), 'armature.collections should reflect only the roots')
|
|
|
|
|
self.assertEqual([root2], list(r1_child1.children))
|
|
|
|
|
self.assertEqual([r1_child1, r1_child1_001], list(root1.children), 'root1.children should have its children')
|
|
|
|
|
self.assertEqual([r2_child1, r2_child2], list(root2.children), 'root2.children should have its children')
|
2024-01-02 12:47:35 +01:00
|
|
|
self.assertEqual(r1_child1, root2.parent)
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
# Check the array order.
|
2024-01-11 14:36:59 +01:00
|
|
|
self.assertEqual([root1, r1_child1, r1_child1_001, r2_child1, r2_child2, root2], list(bcolls_all))
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
# Move root2 between r1_child1 and r1_child1_001.
|
2024-01-05 16:51:26 +01:00
|
|
|
root2.parent = root1
|
2024-01-11 14:36:59 +01:00
|
|
|
root2.child_number = 1
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
# Check the hierarchy.
|
|
|
|
|
self.assertEqual([root1], list(bcolls), 'armature.collections should reflect only the roots')
|
|
|
|
|
self.assertEqual([r1_child1, root2, r1_child1_001], list(
|
|
|
|
|
root1.children), 'root1.children should have its children')
|
|
|
|
|
self.assertEqual([r2_child1, r2_child2], list(root2.children), 'root2.children should have its children')
|
|
|
|
|
|
|
|
|
|
# Check the array order.
|
2024-01-11 14:36:59 +01:00
|
|
|
self.assertEqual([root1, r1_child1, root2, r1_child1_001, r2_child1, r2_child2], list(bcolls_all))
|
2023-12-29 12:31:34 +01:00
|
|
|
|
2024-01-04 15:33:18 +01:00
|
|
|
def test_parent_property(self):
|
|
|
|
|
# Just to keep the rest of the code shorter.
|
|
|
|
|
bcolls = self.arm.collections
|
|
|
|
|
|
|
|
|
|
self.assertEqual([], list(bcolls), "By default an Armature should have no collections")
|
|
|
|
|
|
|
|
|
|
# Build a hierarchy.
|
|
|
|
|
root1 = bcolls.new('root1')
|
|
|
|
|
r1_child1 = bcolls.new('r1_child1', parent=root1)
|
|
|
|
|
r1_child2 = bcolls.new('r1_child2', parent=root1)
|
|
|
|
|
root2 = bcolls.new('root2')
|
|
|
|
|
r2_child1 = bcolls.new('r2_child1', parent=root2)
|
|
|
|
|
r2_child2 = bcolls.new('r2_child2', parent=root2)
|
|
|
|
|
|
|
|
|
|
# Check getting the parent.
|
|
|
|
|
self.assertEqual(root1, r1_child1.parent)
|
|
|
|
|
self.assertEqual(root1, r1_child2.parent)
|
|
|
|
|
self.assertEqual(root2, r2_child1.parent)
|
|
|
|
|
self.assertEqual(root2, r2_child2.parent)
|
|
|
|
|
|
|
|
|
|
# Move r1_child1 to be a child of root2 by assigning the parent.
|
|
|
|
|
r1_child1.parent = root2
|
|
|
|
|
self.assertEqual(root2, r1_child1.parent)
|
|
|
|
|
|
|
|
|
|
# Check the sibling order.
|
|
|
|
|
self.assertEqual([r2_child1, r2_child2, r1_child1], list(root2.children))
|
|
|
|
|
|
|
|
|
|
# Make r1_child1 a root.
|
|
|
|
|
r1_child1.parent = None
|
|
|
|
|
self.assertIsNone(r1_child1.parent)
|
|
|
|
|
|
2024-01-02 15:40:30 +01:00
|
|
|
def test_bone_collection_bones(self):
|
|
|
|
|
# Build a hierarchy on the armature.
|
|
|
|
|
bcolls = self.arm.collections
|
|
|
|
|
bcoll_root = bcolls.new('root')
|
|
|
|
|
bcoll_child1 = bcolls.new('child1', parent=bcoll_root)
|
2024-01-02 15:57:42 +01:00
|
|
|
bcoll_child2 = bcolls.new('child2', parent=bcoll_child1)
|
2024-01-02 15:40:30 +01:00
|
|
|
|
|
|
|
|
# Add bones to the armature & assign to collections.
|
|
|
|
|
bone_dict = self.add_bones(self.arm_ob)
|
|
|
|
|
bcoll_root.assign(bone_dict['root'])
|
|
|
|
|
|
2024-01-02 15:57:42 +01:00
|
|
|
bcoll_child1_bone_names = {'child_L', 'child_L_L', 'child_L_R'}
|
2024-01-02 15:40:30 +01:00
|
|
|
for bone_name in bcoll_child1_bone_names:
|
|
|
|
|
bcoll_child1.assign(bone_dict[bone_name])
|
|
|
|
|
|
2024-01-02 15:57:42 +01:00
|
|
|
bcoll_child2_bone_names = {'child_R', 'child_R_L', 'child_R_R'}
|
2024-01-02 15:40:30 +01:00
|
|
|
for bone_name in bcoll_child2_bone_names:
|
|
|
|
|
bcoll_child2.assign(bone_dict[bone_name])
|
|
|
|
|
|
|
|
|
|
# Check that the `.bones` property returns the expected ones.
|
|
|
|
|
self.assertEqual([self.arm.bones['root']], list(bcoll_root.bones))
|
2024-01-02 15:57:42 +01:00
|
|
|
self.assertEqual(bcoll_child1_bone_names, {b.name for b in bcoll_child1.bones})
|
|
|
|
|
self.assertEqual(bcoll_child2_bone_names, {b.name for b in bcoll_child2.bones})
|
|
|
|
|
|
|
|
|
|
# Check that the `.bones_recursive` property returns the expected bones.
|
|
|
|
|
all_bones = set(self.arm.bones)
|
|
|
|
|
self.assertEqual(all_bones, set(bcoll_root.bones_recursive),
|
|
|
|
|
'All bones should have been assigned to at least one bone collection')
|
|
|
|
|
|
|
|
|
|
self.assertEqual(bcoll_child1_bone_names | bcoll_child2_bone_names,
|
|
|
|
|
{b.name for b in bcoll_child1.bones_recursive},
|
|
|
|
|
"All bones of child1 and child2 should be in child1.bones_recursive")
|
|
|
|
|
|
|
|
|
|
self.assertEqual(bcoll_child2_bone_names, {b.name for b in bcoll_child2.bones_recursive})
|
2024-01-02 15:40:30 +01:00
|
|
|
|
2023-12-29 12:31:34 +01:00
|
|
|
def test_bone_collection_armature_join(self):
|
|
|
|
|
other_arm_ob, other_arm = self.create_armature()
|
|
|
|
|
|
|
|
|
|
# Build a hierarchy on the main armature.
|
|
|
|
|
main_bcolls = self.arm.collections
|
|
|
|
|
main_root = main_bcolls.new('root')
|
|
|
|
|
main_child1 = main_bcolls.new('child1', parent=main_root)
|
|
|
|
|
main_child2 = main_bcolls.new('child2', parent=main_root)
|
|
|
|
|
|
|
|
|
|
# Build a hierarchy on the other armature.
|
|
|
|
|
other_bcolls = other_arm.collections
|
|
|
|
|
other_root = other_bcolls.new('root')
|
|
|
|
|
other_child1 = other_bcolls.new('child1', parent=other_root)
|
|
|
|
|
other_child3 = other_bcolls.new('child3', parent=other_root)
|
|
|
|
|
|
|
|
|
|
# Create some custom properties on the collections.
|
|
|
|
|
main_root['float'] = 0.2
|
|
|
|
|
main_child1['string'] = 'main_string'
|
|
|
|
|
main_child2['dict'] = {'agent': 47}
|
|
|
|
|
|
|
|
|
|
other_root['float'] = 0.42
|
|
|
|
|
other_child1['strange'] = 'other_string'
|
|
|
|
|
other_child3['dict'] = {'agent': 327}
|
|
|
|
|
|
|
|
|
|
# Join the two armatures together.
|
|
|
|
|
self.assertEqual({'FINISHED'}, bpy.ops.object.select_all(action='DESELECT'))
|
|
|
|
|
bpy.context.view_layer.objects.active = self.arm_ob
|
|
|
|
|
self.arm_ob.select_set(True)
|
|
|
|
|
other_arm_ob.select_set(True)
|
|
|
|
|
self.assertEqual({'FINISHED'}, bpy.ops.object.join())
|
|
|
|
|
|
|
|
|
|
# Check the custom properties.
|
2024-01-11 14:36:59 +01:00
|
|
|
bcolls_all = self.arm.collections_all
|
|
|
|
|
self.assertEqual(0.2, bcolls_all['root']['float'])
|
|
|
|
|
self.assertEqual('main_string', bcolls_all['child1']['string'])
|
|
|
|
|
self.assertEqual({'agent': 47}, bcolls_all['child2']['dict'].to_dict())
|
2023-12-29 12:31:34 +01:00
|
|
|
self.assertNotIn(
|
|
|
|
|
'strange',
|
2024-01-11 14:36:59 +01:00
|
|
|
bcolls_all['child1'],
|
2023-12-29 12:31:34 +01:00
|
|
|
'Bone collections that already existed in the active armature are not expected to be updated')
|
2024-01-11 14:36:59 +01:00
|
|
|
self.assertEqual({'agent': 327}, bcolls_all['child3']['dict'].to_dict())
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
|
2025-10-13 12:12:25 +02:00
|
|
|
class ArmatureCreationTest(unittest.TestCase):
|
|
|
|
|
arm_ob: Object
|
|
|
|
|
arm: Armature
|
|
|
|
|
|
|
|
|
|
def setUp(self) -> None:
|
|
|
|
|
print("\033[92mloading empty homefile\033[0m")
|
|
|
|
|
bpy.ops.wm.read_homefile(use_empty=True, use_factory_startup=True)
|
|
|
|
|
self.arm_ob, self.arm = self.create_armature()
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def create_armature() -> tuple[Object, Armature]:
|
|
|
|
|
"""Create an Armature without any bones."""
|
|
|
|
|
arm = bpy.data.armatures.new('Armature')
|
|
|
|
|
arm_ob = bpy.data.objects.new('ArmObject', arm)
|
|
|
|
|
|
|
|
|
|
# Remove any pre-existing bone.
|
|
|
|
|
while arm.bones:
|
|
|
|
|
arm.bones.remove(arm.bones[0])
|
|
|
|
|
|
|
|
|
|
bpy.context.scene.collection.objects.link(arm_ob)
|
|
|
|
|
bpy.context.view_layer.objects.active = arm_ob
|
|
|
|
|
|
|
|
|
|
return arm_ob, arm
|
|
|
|
|
|
|
|
|
|
def create_bone(self, name: str, parent: EditBone | None, head: Vectorish, tail: Vectorish) -> EditBone:
|
|
|
|
|
bone = self.arm.edit_bones.new(name)
|
|
|
|
|
bone.parent = parent
|
|
|
|
|
bone.head = head
|
|
|
|
|
bone.tail = tail
|
|
|
|
|
bone.use_connect = True
|
|
|
|
|
return bone
|
|
|
|
|
|
|
|
|
|
def test_tiny_bones(self) -> None:
|
|
|
|
|
"""Tiny bones should be elongated."""
|
|
|
|
|
|
|
|
|
|
# 'bpy.context.active_object' does not exist when Blender is running in
|
|
|
|
|
# GUI mode. That's not the normal way to run this test, but very useful
|
|
|
|
|
# to be able to do for debugging purposes.
|
|
|
|
|
with bpy.context.temp_override(active_object=self.arm_ob):
|
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
|
|
|
|
|
|
# Constants defined in `ED_armature_from_edit()`:
|
|
|
|
|
bone_length_threshold = 0.000001
|
|
|
|
|
adjusted_bone_length = 2 * bone_length_threshold
|
|
|
|
|
|
|
|
|
|
# A value for which the vector (under_threshold, 0, under_threshold)
|
|
|
|
|
# is still shorter than the bone length threshold.
|
|
|
|
|
under_threshold = 0.0000006
|
|
|
|
|
|
|
|
|
|
root = self.create_bone("root", None, (0, 0, 0), (0, 0, 1))
|
|
|
|
|
|
|
|
|
|
tinychild_1 = self.create_bone(
|
|
|
|
|
"tinychild_1",
|
|
|
|
|
root,
|
|
|
|
|
root.tail,
|
|
|
|
|
root.tail + Vector((0, under_threshold, 0)),
|
|
|
|
|
)
|
|
|
|
|
self.create_bone(
|
|
|
|
|
"tinychild_2",
|
|
|
|
|
root,
|
|
|
|
|
root.tail,
|
|
|
|
|
root.tail + Vector((under_threshold, 0, under_threshold)),
|
|
|
|
|
)
|
|
|
|
|
self.create_bone(
|
|
|
|
|
"zerochild_3",
|
|
|
|
|
root,
|
|
|
|
|
root.tail,
|
|
|
|
|
root.tail,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Give a tiny child a grandchild that is also tiny, in a perpendicular direction.
|
|
|
|
|
self.create_bone(
|
|
|
|
|
"tinygrandchild_1_1",
|
|
|
|
|
tinychild_1,
|
|
|
|
|
tinychild_1.tail,
|
|
|
|
|
tinychild_1.tail + Vector((under_threshold, 0, 0)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Add a grandchild that is long enough.
|
|
|
|
|
grandchild_1_2 = self.create_bone(
|
|
|
|
|
"grandchild_1_2",
|
|
|
|
|
tinychild_1,
|
|
|
|
|
tinychild_1.tail,
|
|
|
|
|
tinychild_1.tail + Vector((1, 0, 0)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Add a great-grandchild, it should remain connected to its parent.
|
|
|
|
|
self.create_bone(
|
|
|
|
|
"great_grandchild_1_2_1",
|
|
|
|
|
grandchild_1_2,
|
|
|
|
|
grandchild_1_2.tail,
|
|
|
|
|
grandchild_1_2.tail + Vector((1, 0, 0)),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Switch out and back into Armature Edit mode, to see how the bones survived the round-trip.
|
|
|
|
|
with bpy.context.temp_override(active_object=self.arm_ob):
|
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
|
|
|
|
|
|
# Check that all bones still exist, and have the expected head/tail. This
|
|
|
|
|
# comparison is done again in Armature Edit mode, so that all the numbers
|
|
|
|
|
# mean the same thing as they meant when creating the bones.
|
|
|
|
|
|
|
|
|
|
actual_names = sorted(bone.name for bone in self.arm.edit_bones)
|
|
|
|
|
expect_names = sorted(["root", "tinychild_1", "tinychild_2", "zerochild_3", "tinygrandchild_1_1",
|
|
|
|
|
"grandchild_1_2", "great_grandchild_1_2_1"])
|
|
|
|
|
self.assertEqual(expect_names, actual_names)
|
|
|
|
|
|
|
|
|
|
def check_bone(
|
|
|
|
|
name: str,
|
|
|
|
|
expect_head: Vectorish,
|
|
|
|
|
expect_tail: Vectorish,
|
|
|
|
|
*,
|
|
|
|
|
expect_connected: bool,
|
|
|
|
|
msg: str,
|
|
|
|
|
places=7,
|
|
|
|
|
) -> None:
|
|
|
|
|
bone = self.arm.edit_bones[name]
|
|
|
|
|
# Convert to tuples for nicer printing in failure messages.
|
|
|
|
|
actual_head = bone.head.to_tuple()
|
|
|
|
|
actual_tail = bone.tail.to_tuple()
|
|
|
|
|
|
|
|
|
|
head_msg = "\n{}:\n Expected head ({:.8f}, {:.8f}, {:.8f}),\n Actual is ({:.8f}, {:.8f}, {:.8f}).\n {}".format(
|
|
|
|
|
name, expect_head[0], expect_head[1], expect_head[2], actual_head[0], actual_head[1], actual_head[2], msg)
|
|
|
|
|
self.assertAlmostEqual(expect_head[0], actual_head[0], places=places, msg=head_msg)
|
|
|
|
|
self.assertAlmostEqual(expect_head[1], actual_head[1], places=places, msg=head_msg)
|
|
|
|
|
self.assertAlmostEqual(expect_head[2], actual_head[2], places=places, msg=head_msg)
|
|
|
|
|
# print("\n{}:\n Head is ({:.8f}, {:.8f}, {:.8f})".format(
|
|
|
|
|
# name, actual_head[0], actual_head[1], actual_head[2]))
|
|
|
|
|
|
|
|
|
|
tail_msg = "\n{}:\n Expected tail ({:.8f}, {:.8f}, {:.8f}),\n Actual is ({:.8f}, {:.8f}, {:.8f}).\n {}".format(
|
|
|
|
|
name, expect_tail[0], expect_tail[1], expect_tail[2], actual_tail[0], actual_tail[1], actual_tail[2], msg)
|
|
|
|
|
self.assertAlmostEqual(expect_tail[0], actual_tail[0], places=places, msg=tail_msg)
|
|
|
|
|
self.assertAlmostEqual(expect_tail[1], actual_tail[1], places=places, msg=tail_msg)
|
|
|
|
|
self.assertAlmostEqual(expect_tail[2], actual_tail[2], places=places, msg=tail_msg)
|
|
|
|
|
# print(" Tail is ({:.8f}, {:.8f}, {:.8f})".format(
|
|
|
|
|
# actual_tail[0], actual_tail[1], actual_tail[2]))
|
|
|
|
|
|
|
|
|
|
self.assertEqual(expect_connected, bone.use_connect, msg="{}: {}".format(bone.name, msg))
|
|
|
|
|
|
|
|
|
|
check_bone("root", (0, 0, 0), (0, 0, 1),
|
|
|
|
|
expect_connected=True, msg="Should not have changed.")
|
|
|
|
|
check_bone("tinychild_1", (0, 0, 1), (0, adjusted_bone_length, 1),
|
|
|
|
|
expect_connected=True, msg="Should have been elongated in the Y-direction")
|
|
|
|
|
|
|
|
|
|
adjust = (Vector((under_threshold, 0, under_threshold)).normalized() * adjusted_bone_length).x
|
|
|
|
|
check_bone("tinychild_2",
|
|
|
|
|
(0, 0, 1),
|
|
|
|
|
(adjust, 0, 1 + adjust),
|
|
|
|
|
expect_connected=True,
|
|
|
|
|
msg="Should have been elongated in the XZ-direction")
|
|
|
|
|
|
|
|
|
|
check_bone("zerochild_3",
|
|
|
|
|
(0, 0, 1),
|
|
|
|
|
(0, 0, 1 + adjusted_bone_length),
|
|
|
|
|
expect_connected=True,
|
|
|
|
|
msg="Should have been elongated in the Z-direction")
|
|
|
|
|
|
|
|
|
|
check_bone("tinygrandchild_1_1",
|
|
|
|
|
(0, under_threshold, 1),
|
|
|
|
|
(adjusted_bone_length, under_threshold, 1),
|
|
|
|
|
expect_connected=False,
|
|
|
|
|
msg="Should have been elongated in the X-direction and disconnected")
|
|
|
|
|
|
|
|
|
|
check_bone("grandchild_1_2",
|
|
|
|
|
(0, under_threshold, 1),
|
|
|
|
|
(1, under_threshold, 1),
|
|
|
|
|
expect_connected=False,
|
|
|
|
|
msg="Should been disconnected")
|
|
|
|
|
|
|
|
|
|
check_bone("great_grandchild_1_2_1",
|
|
|
|
|
(1, under_threshold, 1),
|
|
|
|
|
(2, under_threshold, 1),
|
|
|
|
|
expect_connected=True,
|
|
|
|
|
msg="Should been kept connected")
|
|
|
|
|
|
|
|
|
|
|
2023-12-29 12:31:34 +01:00
|
|
|
def main():
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
if '--' in sys.argv:
|
|
|
|
|
argv = [sys.argv[0]] + sys.argv[sys.argv.index('--') + 1:]
|
|
|
|
|
else:
|
|
|
|
|
# Avoid passing all of Blender's arguments to unittest.main()
|
|
|
|
|
argv = [sys.argv[0]]
|
|
|
|
|
|
2025-10-13 12:12:25 +02:00
|
|
|
unittest.main(argv=argv, exit=False)
|
2023-12-29 12:31:34 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|