Aim controller rays along the laser, not the controller

The Frame controllers' laser comes from their render model's tip, 40
degrees below the controller pose's forward axis. ft-screens tested rays
along the pose, so a controller's laser near a screen's controls didn't
reveal them, and dragging the resize tab or the roll knob with a
controller followed the wrong point. Rays now start from the tip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-09-26 19:42:30 -06:00
1 parent 98667c7650
commit 54b1e5cd4b
2 files changed
+49 -5

No files matched your search

+2
View File
@@ -38,6 +38,8 @@ The curved layout chains screens edge to edge, like monitors on a desk: the midd
A resize handle has to be able to shrink a screen from any direction, so the dragged corner follows the laser along the screen's diagonal rather than taking the larger of its horizontal and vertical reach. Pushing and pulling a carried screen moves it along the line from your head, because the 3D mouse's virtual controller sits just in front of the bar, below the screen's centre, so the line from the device points mostly upward.
Wherever ft-screens needs to know where a laser points (showing the controls, the resize tab, the roll knob), it uses the laser's own pose, the render model's `tip` component, rather than the controller's pose. On the Frame's controllers the tip points 40° below the pose's forward axis, so rays from the pose missed what the laser was actually on. The 3D mouse's virtual controller has no tip, and its laser runs along its pose.
`ComputeOverlayIntersection` ignores `SetOverlayIntersectionMask`, and a control can't be allowed to cover part of its screen, so the resize tab sits entirely outside the corner.
### Wrist pinning
+47 -5
View File
@@ -44,6 +44,7 @@
#include <linux/input-event-codes.h>
#include <algorithm>
#include <chrono>
#include <array>
#include <cmath>
#include <cstdio>
@@ -56,6 +57,7 @@
namespace {
using Mat = vr::HmdMatrix34_t;
using Clock = std::chrono::steady_clock;
Mat Identity() {
Mat m{};
@@ -120,6 +122,42 @@ bool DevicePose(vr::TrackedDeviceIndex_t dev, Mat *out) {
*out = g_poses[dev].mDeviceToAbsoluteTracking;
return true;
}
// Where a device's laser starts and points: SteamVR's laser comes from its render model's
// "tip" component, not the device pose. On the Frame's controllers the tip points 40 degrees
// below the pose's -Z, so rays from the pose missed what the laser was on. Devices without
// a tip (the 3D mouse's virtual controller) aim along their pose. Cached per device; a
// model that isn't loaded yet is asked again a few seconds later.
struct Tip {
std::string model;
Mat offset = Identity();
bool found = false;
Clock::time_point checked;
};
Mat TipOffset(vr::TrackedDeviceIndex_t dev) {
static std::map<vr::TrackedDeviceIndex_t, Tip> cache;
char model[256] = "";
vr::VRSystem()->GetStringTrackedDeviceProperty(dev, vr::Prop_RenderModelName_String, model, sizeof model);
const auto now = Clock::now();
auto it = cache.find(dev);
if (it != cache.end() && it->second.model == model &&
(it->second.found || now - it->second.checked < std::chrono::seconds(5)))
return it->second.offset;
Tip tip{model, Identity(), false, now};
vr::RenderModel_ControllerMode_State_t mode{};
vr::RenderModel_ComponentState_t state{};
if (model[0] && vr::VRRenderModels()->GetComponentStateForDevicePath(model, vr::k_pch_Controller_Component_Tip,
vr::k_ulInvalidInputValueHandle, &mode, &state))
tip.offset = state.mTrackingToComponentLocal, tip.found = true;
cache[dev] = tip;
return tip.offset;
}
bool LaserPose(vr::TrackedDeviceIndex_t dev, Mat *out) {
Mat d;
if (!DevicePose(dev, &d)) return false;
*out = Mul(d, TipOffset(dev));
return true;
}
bool IsHandController(vr::TrackedDeviceIndex_t i) {
if (vr::VRSystem()->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) return false;
char type[64] = "";
@@ -641,7 +679,7 @@ void UpdateControls() {
std::vector<Mat> lasers;
for (vr::TrackedDeviceIndex_t i = 1; i < vr::k_unMaxTrackedDeviceCount; ++i) {
Mat d;
if (vr::VRSystem()->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller && DevicePose(i, &d))
if (vr::VRSystem()->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller && LaserPose(i, &d))
lasers.push_back(d);
}
for (auto &[i, s] : g_screens) {
@@ -792,12 +830,14 @@ void StartDrag(Screen &s, Drag mode, vr::TrackedDeviceIndex_t dev) {
LightBar(s, s.pinTarget != kNone);
if (mode == Drag::Resize) {
double hx, hy;
if (RayOnScreen(s, d, &hx, &hy)) s.grabX = hx - s.metres / 2, s.grabY = hy + s.heightMetres() / 2;
Mat l;
if (LaserPose(dev, &l) && RayOnScreen(s, l, &hx, &hy)) s.grabX = hx - s.metres / 2, s.grabY = hy + s.heightMetres() / 2;
else s.grabX = s.grabY = 0;
}
if (mode == Drag::Roll) {
s.rollFrom = s.pinned != kNone ? s.pinRel : p;
if (!RollLaserAngle(s, d, &s.rollAngle)) s.drag = Drag::None, s.dragDevice = kNone;
Mat l;
if (!LaserPose(dev, &l) || !RollLaserAngle(s, l, &s.rollAngle)) s.drag = Drag::None, s.dragDevice = kNone;
}
ApplyAlpha(s);
}
@@ -893,14 +933,16 @@ void UpdateDrag(Screen &s, int index) {
if (s.drag == Drag::Roll) {
// Like turning a knob: the screen turns as far as the laser has gone around its centre.
double a;
if (!RollLaserAngle(s, d, &a)) return;
Mat l;
if (!LaserPose(s.dragDevice, &l) || !RollLaserAngle(s, l, &a)) return;
ApplyRoll(s, std::remainder(a - s.rollAngle, 2 * M_PI));
return;
}
// Resize: the corner follows the ray along the screen's diagonal (so it shrinks and
// grows from any direction), keeping where on the handle it was grabbed.
double hx, hy;
if (!RayOnScreen(s, d, &hx, &hy)) return;
Mat l;
if (!LaserPose(s.dragDevice, &l) || !RayOnScreen(s, l, &hx, &hy)) return;
const double a = s.width > 0 ? double(s.height) / s.width : 9.0 / 16;
const double cx = hx - s.grabX, cy = hy - s.grabY; // where the corner should be
SetWidth(s, 2 * (cx - a * cy) / (1 + a * a));