From 29f3729df7e1f946d08326558f13e19112fa7687 Mon Sep 17 00:00:00 2001 From: baketnk Date: Thu, 24 Sep 2026 14:54:13 -0400 Subject: [PATCH] Face right-wrist panel inward without mirroring text --- docs/overlay.md | 6 ++++-- src/mount.cpp | 6 ++++++ tests/mount_test.cpp | 26 ++++++++++++++++++++++++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/overlay.md b/docs/overlay.md index 2f7a3f7..9d302e2 100644 --- a/docs/overlay.md +++ b/docs/overlay.md @@ -173,10 +173,12 @@ rather than placing a menu at the world origin. A tracking-origin reset requests a fresh placement. Settings → Recenter in front deliberately resamples the pose. Settings offers World space, Left wrist, Right wrist and Head on that same canvas. -World/head width is 0.85 m; wrist width defaults to 0.30 m. Both wrists use +World/head width is 0.85 m; wrist width defaults to 0.30 m. The left wrist uses VR Workspace's fallback watch-face axes: panel-right points toward the fingers (controller -Z), panel-up points out of the back of the hand (controller +Y), -and panel-front points toward controller +X. Its controller-relative center is +and panel-front points toward controller +X. The right wrist reverses panel-right +and panel-front (controller +Z and -X), keeping panel-up unchanged so it faces +inward with upright, unmirrored text. The controller-relative center is (0, 0.18, 0.089) m, approximating the compact HUD's surface center: its 0.12 m wrist lift, 0.09 m bottom anchor and ~0.03 m panel-center correction; Z combines the fallback 0.054 m wrist calibration and 0.035 m finger-back offset. This copies placement geometry, not diff --git a/src/mount.cpp b/src/mount.cpp index 9244c61..b9bbcc5 100644 --- a/src/mount.cpp +++ b/src/mount.cpp @@ -78,6 +78,12 @@ Matrix34 relative_mount_pose(Mount mount, const WristPlacement& wrist) { pose[0] = {0.f, s, c, c * wrist.x + s * wrist.y}; pose[1] = {0.f, c, -s, -s * wrist.x + c * wrist.y}; pose[2] = {-1.f, 0.f, 0.f, wrist.z}; + if (mount == Mount::RightWrist) { + // The wearer views the right wrist from the opposite side. Rotate + // 180 degrees about panel-up, not a reflection or upside-down roll: + // both right and front reverse, while up and center stay unchanged. + for (auto& row : pose) { row[0] = -row[0]; row[2] = -row[2]; } + } break; } case Mount::World: break; diff --git a/tests/mount_test.cpp b/tests/mount_test.cpp index 79b8b02..3366249 100644 --- a/tests/mount_test.cpp +++ b/tests/mount_test.cpp @@ -66,7 +66,13 @@ int main() { check_basis(head, 1, 0, 0, 1); CHECK(near(head[0][3], 0)); CHECK(near(head[1][3], -.16f)); CHECK(near(head[2][3], -1.05f)); const auto left = relative_mount_pose(Mount::LeftWrist), right = relative_mount_pose(Mount::RightWrist); - CHECK(left == right); // VR Workspace fallback calibration uses the same controller-local axes for both hands. + check_basis(right, 0, 1, -1, 0); // right wrist faces inward, not away from wearer + for (int r = 0; r < 3; ++r) { + CHECK(near(right[r][3], left[r][3])); // position and up unchanged + CHECK(near(right[r][1], left[r][1])); + CHECK(near(right[r][0], -left[r][0])); + CHECK(near(right[r][2], -left[r][2])); + } CHECK(near(left[0][0], 0)); CHECK(near(left[1][0], 0)); CHECK(near(left[2][0], -1)); CHECK(near(left[0][1], 0)); CHECK(near(left[1][1], 1)); CHECK(near(left[2][1], 0)); CHECK(near(left[0][2], 1)); CHECK(near(left[1][2], 0)); CHECK(near(left[2][2], 0)); @@ -74,7 +80,23 @@ int main() { WristPlacement tuned{.02f, .1f, .07f, .25f, 90.f}; const auto rotated = relative_mount_pose(Mount::RightWrist, tuned); CHECK(near(rotated[0][1], 1)); CHECK(near(rotated[1][1], 0)); - CHECK(near(rotated[0][2], 0)); CHECK(near(rotated[1][2], -1)); + CHECK(near(rotated[0][2], 0)); CHECK(near(rotated[1][2], 1)); + // Every roll remains an orthonormal, positive-determinant rotation: no mirror. + for (float roll : {-180.f, -90.f, -35.f, 0.f, 90.f, 180.f}) { + tuned.roll_degrees = roll; + for (auto hand : {Mount::LeftWrist, Mount::RightWrist}) { + const auto m = relative_mount_pose(hand, tuned); + for (int a = 0; a < 3; ++a) for (int b = 0; b < 3; ++b) { + float dot = 0; + for (int r = 0; r < 3; ++r) dot += m[r][a] * m[r][b]; + CHECK(near(dot, a == b ? 1.f : 0.f)); + } + for (int r = 0; r < 3; ++r) { + const int a = (r + 1) % 3, b = (r + 2) % 3; + CHECK(near(m[a][0] * m[b][1] - m[b][0] * m[a][1], m[r][2])); + } + } + } CHECK(near(rotated[0][3], .1f)); CHECK(near(rotated[1][3], -.02f)); CHECK(near(rotated[2][3], .07f)); CHECK(near(mount_width(Mount::RightWrist, tuned), .25f)); CHECK(relative_mount_pose(Mount::Head, tuned) == head);