Skip to content

Commit 6d0eaf6

Browse files
cdtwiggmeta-codesync[bot]
authored andcommitted
Fix Skeleton::commonAncestor to return kInvalidIndex for invalid inputs (#1045)
Summary: Pull Request resolved: #1045 Previously, commonAncestor(validJoint, kInvalidIndex) would return validJoint, which is incorrect — there is no common ancestor when one of the joints doesn't exist in the hierarchy. The old implementation exited the while loop immediately when either input was kInvalidIndex (due to the `joint != kInvalidIndex` condition) and returned whichever value was in joint1. This was effectively a bug, though no existing callers triggered it since they always passed valid joint indices. The fix adds an early return for kInvalidIndex inputs and simplifies the loop condition. Reviewed By: cstollmeta Differential Revision: D93159998 fbshipit-source-id: 32b70761f9250c88192cc4d2e2edde37fa3b1548
1 parent 9956677 commit 6d0eaf6

File tree

3 files changed

+55
-3
lines changed

3 files changed

+55
-3
lines changed

momentum/character/skeleton.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,11 @@ size_t SkeletonT<T>::commonAncestor(size_t joint1, size_t joint2) const {
8686
MT_CHECK(joint1 == kInvalidIndex || joint1 < joints.size());
8787
MT_CHECK(joint2 == kInvalidIndex || joint2 < joints.size());
8888

89-
while (joint1 != kInvalidIndex && joint2 != kInvalidIndex && joint1 != joint2) {
90-
if (joint1 < joint2) {
89+
while (joint1 != joint2) {
90+
// If either joint is invalid, there is no common ancestor.
91+
if (joint1 == kInvalidIndex || joint2 == kInvalidIndex) {
92+
return kInvalidIndex;
93+
} else if (joint1 < joint2) {
9194
joint2 = joints[joint2].parent;
9295
} else {
9396
joint1 = joints[joint1].parent;

momentum/character/skeleton.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ struct SkeletonT {
6464
/// Finds the closest common ancestor of two joints in the hierarchy.
6565
///
6666
/// Returns the index of the joint that is the lowest common ancestor
67-
/// in the hierarchy for the two specified joints.
67+
/// in the hierarchy for the two specified joints. Returns kInvalidIndex
68+
/// if either joint is kInvalidIndex.
6869
[[nodiscard]] size_t commonAncestor(size_t joint1, size_t joint2) const;
6970

7071
/// Converts the skeleton to use a different scalar type.

momentum/test/character/skeleton_test.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,54 @@ TYPED_TEST(SkeletonTest, CommonAncestor) {
269269
// Test common ancestor of cousins
270270
EXPECT_EQ(skeleton.commonAncestor(2, 4), 0); // Common ancestor of Joint 2 and Joint 4 is Root
271271
EXPECT_EQ(skeleton.commonAncestor(4, 2), 0); // Common ancestor of Joint 4 and Joint 2 is Root
272+
273+
// Test with kInvalidIndex — no common ancestor exists
274+
EXPECT_EQ(skeleton.commonAncestor(0, kInvalidIndex), kInvalidIndex);
275+
EXPECT_EQ(skeleton.commonAncestor(kInvalidIndex, 0), kInvalidIndex);
276+
EXPECT_EQ(skeleton.commonAncestor(kInvalidIndex, kInvalidIndex), kInvalidIndex);
277+
}
278+
279+
// Test commonAncestor with disjoint trees (joints that don't share a root)
280+
TYPED_TEST(SkeletonTest, CommonAncestorDisjointTrees) {
281+
// Create a skeleton with two disjoint trees:
282+
// Joint 0: Root A
283+
// Joint 1: Child of Root A
284+
// Joint 2: Root B (parent = kInvalidIndex, a separate tree)
285+
// Joint 3: Child of Root B
286+
std::vector<Joint> disjointJoints(4);
287+
288+
disjointJoints[0].name = "rootA";
289+
disjointJoints[0].parent = kInvalidIndex;
290+
disjointJoints[0].translationOffset = Vector3<float>::Zero();
291+
disjointJoints[0].preRotation = Quaternion<float>::Identity();
292+
293+
disjointJoints[1].name = "childA";
294+
disjointJoints[1].parent = 0;
295+
disjointJoints[1].translationOffset = Vector3<float>(1, 0, 0);
296+
disjointJoints[1].preRotation = Quaternion<float>::Identity();
297+
298+
disjointJoints[2].name = "rootB";
299+
disjointJoints[2].parent = kInvalidIndex;
300+
disjointJoints[2].translationOffset = Vector3<float>(5, 0, 0);
301+
disjointJoints[2].preRotation = Quaternion<float>::Identity();
302+
303+
disjointJoints[3].name = "childB";
304+
disjointJoints[3].parent = 2;
305+
disjointJoints[3].translationOffset = Vector3<float>(1, 0, 0);
306+
disjointJoints[3].preRotation = Quaternion<float>::Identity();
307+
308+
using SkeletonType = typename TestFixture::SkeletonType;
309+
SkeletonType disjointSkeleton(disjointJoints);
310+
311+
// Joints within the same tree should find a common ancestor
312+
EXPECT_EQ(disjointSkeleton.commonAncestor(0, 1), 0);
313+
EXPECT_EQ(disjointSkeleton.commonAncestor(2, 3), 2);
314+
315+
// Joints across disjoint trees should return kInvalidIndex
316+
EXPECT_EQ(disjointSkeleton.commonAncestor(0, 2), kInvalidIndex);
317+
EXPECT_EQ(disjointSkeleton.commonAncestor(1, 3), kInvalidIndex);
318+
EXPECT_EQ(disjointSkeleton.commonAncestor(0, 3), kInvalidIndex);
319+
EXPECT_EQ(disjointSkeleton.commonAncestor(1, 2), kInvalidIndex);
272320
}
273321

274322
// Test cast method

0 commit comments

Comments
 (0)