mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 00:00:07 +02:00
Several KDE Plasma screens floating in SteamVR, each a real monitor of any resolution and shape, shown by our own compositor (ft-screens), with a layout, wrist pinning, and visibility modes; a Bluetooth mouse that drives all of SteamVR as a room-anchored 3D pointer (input relay, ft-pointer helper, ft_pointer SteamVR driver); two settings apps; and Bluetooth LE fixes. Installs on the headset with ./install.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1276 lines
59 KiB
C++
1276 lines
59 KiB
C++
// The OpenVR side of ft-screens: one overlay per screen, client DMA-BUFs imported with
|
|
// IVRIPCResourceManagerClient::ImportDmabuf (no copy, no size limit), panel mouse events
|
|
// turned into ft_events for the compositor, and the panels' own handling:
|
|
// - a grab bar under each screen: press it with any laser (a controller, or the 3D
|
|
// mouse's virtual controller) and the screen follows that device rigidly until the
|
|
// release, so the 3D mouse's tilt (right button while dragging) turns it; scrolling
|
|
// while dragging pushes it away or pulls it closer (along the line from the head).
|
|
// - a curve button next to the bar: bends the screen into a cylinder around you (its
|
|
// radius: your distance to it when pressed), or flat again.
|
|
// - a roll button next to that: drag it sideways like a knob to roll the screen about
|
|
// its centre (it snaps level within kRollSnap), or scroll on it for kRollStep steps.
|
|
// - a resize tab on the bottom right corner: drag it to set the width (the height
|
|
// follows the screen's resolution).
|
|
// The controls are translucent, like SteamVR's own, and brighten under a laser. They
|
|
// are invisible until a laser (a controller's, or the 3D mouse's) passes very close to
|
|
// one of them (UpdateControls).
|
|
// - pin to a wrist: while carrying a screen, sweep the laser (the line from the carrying
|
|
// device to the bar) across your other controller. A ring around each controller
|
|
// shows the target and a dot where the laser passes it; crossing the ring arms the pin
|
|
// (ring and bar turn blue), crossing it again disarms it. Let go while armed and the
|
|
// screen rides on that controller as it is then, at any size and distance, so you can
|
|
// arm it and then turn it the way you want before letting go. Grabbing a pinned
|
|
// screen keeps it armed for its wrist: move it, let go, and it's re-pinned there
|
|
// (sweep across the ring to take it off). A pinned screen shows only while you see
|
|
// its front, within the wrist angle (and fades out over the last kFade degrees).
|
|
// - visibility modes: always (the hide hotkey toggles), only with the SteamVR dashboard
|
|
// open, while you look at a chosen controller (the wrist gesture), or toggle only
|
|
// (hidden until the hotkey shows them). While visible the screens keep SteamVR's laser
|
|
// mouse on (VROverlayFlags_MakeOverlaysInteractiveIfVisible), so controllers can use
|
|
// them with the dashboard closed; hidden, VR games get their triggers back.
|
|
// OpenVR has no overlay-relative transforms here (openvr v2.15.6), so the bar, button,
|
|
// and handle are placed whenever their screen moves.
|
|
#include "vr.h"
|
|
|
|
#include <openvr.h>
|
|
|
|
#include <linux/input-event-codes.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
using Mat = vr::HmdMatrix34_t;
|
|
|
|
Mat Identity() {
|
|
Mat m{};
|
|
m.m[0][0] = m.m[1][1] = m.m[2][2] = 1;
|
|
return m;
|
|
}
|
|
Mat Mul(const Mat &a, const Mat &b) {
|
|
Mat r{};
|
|
for (int i = 0; i < 3; ++i) {
|
|
for (int j = 0; j < 4; ++j) {
|
|
double v = j == 3 ? a.m[i][3] : 0;
|
|
for (int k = 0; k < 3; ++k) v += a.m[i][k] * b.m[k][j];
|
|
r.m[i][j] = float(v);
|
|
}
|
|
}
|
|
return r;
|
|
}
|
|
Mat Inverse(const Mat &a) { // rigid: R^T, -R^T t
|
|
Mat r{};
|
|
for (int i = 0; i < 3; ++i)
|
|
for (int j = 0; j < 3; ++j) r.m[i][j] = a.m[j][i];
|
|
for (int i = 0; i < 3; ++i) r.m[i][3] = -(r.m[i][0] * a.m[0][3] + r.m[i][1] * a.m[1][3] + r.m[i][2] * a.m[2][3]);
|
|
return r;
|
|
}
|
|
Mat Translation(double x, double y, double z) {
|
|
Mat m = Identity();
|
|
m.m[0][3] = float(x), m.m[1][3] = float(y), m.m[2][3] = float(z);
|
|
return m;
|
|
}
|
|
double Dot3(const double a[3], const double b[3]) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
|
|
void Column(const Mat &m, int c, double out[3]) { out[0] = m.m[0][c], out[1] = m.m[1][c], out[2] = m.m[2][c]; }
|
|
|
|
// A panel pose from a centre and the direction its front is seen from (yaw, pitch; see
|
|
// layout: the front faces back along that direction), turned by roll.
|
|
Mat PanelPose(double x, double y, double z, double yawDeg, double pitchDeg, double rollDeg) {
|
|
const double yw = yawDeg * M_PI / 180, pt = pitchDeg * M_PI / 180, rl = rollDeg * M_PI / 180;
|
|
const double fx = -std::sin(yw) * std::cos(pt), fy = std::sin(pt), fz = -std::cos(yw) * std::cos(pt);
|
|
const double Z[3] = {-fx, -fy, -fz}; // the front
|
|
double X[3] = {Z[2], 0, -Z[0]}; // up x Z: horizontal right
|
|
const double n = std::sqrt(X[0] * X[0] + X[2] * X[2]) + 1e-12;
|
|
X[0] /= n, X[2] /= n;
|
|
const double Y[3] = {Z[1] * X[2] - Z[2] * X[1], Z[2] * X[0] - Z[0] * X[2], Z[0] * X[1] - Z[1] * X[0]};
|
|
const double c = std::cos(rl), s = std::sin(rl);
|
|
Mat m{};
|
|
for (int i = 0; i < 3; ++i) {
|
|
m.m[i][0] = float(X[i] * c + Y[i] * s);
|
|
m.m[i][1] = float(Y[i] * c - X[i] * s);
|
|
m.m[i][2] = float(Z[i]);
|
|
}
|
|
m.m[0][3] = float(x), m.m[1][3] = float(y), m.m[2][3] = float(z);
|
|
return m;
|
|
}
|
|
|
|
// Device poses, read once per tick (ft_vr_poll) or per command.
|
|
vr::TrackedDevicePose_t g_poses[vr::k_unMaxTrackedDeviceCount];
|
|
void RefreshPoses() {
|
|
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, g_poses,
|
|
vr::k_unMaxTrackedDeviceCount);
|
|
}
|
|
bool DevicePose(vr::TrackedDeviceIndex_t dev, Mat *out) {
|
|
if (dev >= vr::k_unMaxTrackedDeviceCount || !g_poses[dev].bPoseIsValid) return false;
|
|
*out = g_poses[dev].mDeviceToAbsoluteTracking;
|
|
return true;
|
|
}
|
|
bool IsHandController(vr::TrackedDeviceIndex_t i) {
|
|
if (vr::VRSystem()->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) return false;
|
|
char type[64] = "";
|
|
vr::VRSystem()->GetStringTrackedDeviceProperty(i, vr::Prop_ControllerType_String, type, sizeof type);
|
|
return std::strcmp(type, "ft_pointer") != 0; // not the 3D mouse's virtual controller
|
|
}
|
|
vr::TrackedDeviceIndex_t HandDevice(const char *hand) {
|
|
return vr::VRSystem()->GetTrackedDeviceIndexForControllerRole(
|
|
std::strcmp(hand, "right") == 0 ? vr::TrackedControllerRole_RightHand : vr::TrackedControllerRole_LeftHand);
|
|
}
|
|
const char *HandName(vr::TrackedDeviceIndex_t i) {
|
|
switch (vr::VRSystem()->GetControllerRoleForTrackedDeviceIndex(i)) {
|
|
case vr::TrackedControllerRole_LeftHand: return "left";
|
|
case vr::TrackedControllerRole_RightHand: return "right";
|
|
default: return "none";
|
|
}
|
|
}
|
|
|
|
enum class Drag { None, Move, Resize, Roll };
|
|
enum class Mode { Always, Dashboard, Gesture, Toggle };
|
|
|
|
constexpr double kWristZone = 0.06; // the laser passing this close to a controller is on its wrist
|
|
constexpr double kWristLeave = 0.09; // ...and has left it beyond this (so it doesn't flicker)
|
|
constexpr double kDotRange = 0.35; // the guide dot shows while the laser is this close
|
|
constexpr double kMinWidth = 0.15;
|
|
constexpr double kFade = 10; // degrees over which a pinned screen fades out
|
|
constexpr double kRollSnap = 2.5; // degrees from level where rolling snaps level
|
|
constexpr double kRollStep = 5; // degrees per scroll notch on the roll button
|
|
constexpr float kChromeIdle = 0.55f; // the controls' opacity without a laser on them
|
|
constexpr long kControlsLinger = 35; // ticks (~0.4 s) the controls stay after a laser leaves
|
|
long g_tick = 0; // ft_vr_poll calls
|
|
constexpr vr::TrackedDeviceIndex_t kNone = vr::k_unTrackedDeviceIndexInvalid;
|
|
|
|
struct Screen {
|
|
vr::VROverlayHandle_t overlay = vr::k_ulOverlayHandleInvalid, bar = vr::k_ulOverlayHandleInvalid,
|
|
handle = vr::k_ulOverlayHandleInvalid, curveButton = vr::k_ulOverlayHandleInvalid,
|
|
rollButton = vr::k_ulOverlayHandleInvalid;
|
|
int width = 0, height = 0; // current buffer size (mouse scale)
|
|
double metres = 1;
|
|
double curve = 0; // cylinder radius in metres; 0 = flat
|
|
const void *shown = nullptr; // a frame arrived
|
|
bool visible = false; // shown in VR right now
|
|
float alpha = 1;
|
|
vr::TrackedDeviceIndex_t pinned = kNone; // riding on this controller
|
|
Mat pinRel = Identity(); // controller -> screen
|
|
Drag drag = Drag::None;
|
|
vr::TrackedDeviceIndex_t dragDevice = kNone;
|
|
Mat dragRel = Identity(); // device -> screen, while moving
|
|
double grabX = 0, grabY = 0; // resize: the grab point relative to the corner
|
|
Mat rollFrom = Identity(); // roll: the pose at the press (pinRel when pinned)
|
|
double rollAngle = 0; // roll: the laser's angle around the centre then
|
|
bool hover[4] = {}; // a laser is on the bar, curve, roll, resize control
|
|
float controls = 0; // the controls' fade, 0 (hidden) .. 1
|
|
bool controlsUp = false; // the controls' overlays are shown
|
|
long nearUntil = 0; // a laser was near the controls until this tick
|
|
vr::TrackedDeviceIndex_t pinTarget = kNone; // moving: rides on this controller when let go
|
|
vr::TrackedDeviceIndex_t onWrist = kNone; // moving: the laser is in this controller's ring
|
|
bool barLit = false;
|
|
double chrome = 0.3; // the bar's width; the other controls follow it (ChromeSize)
|
|
double grip = 0.04; // the corner tab's and the round buttons' size
|
|
double heightMetres() const { return width > 0 ? metres * height / width : metres * 9 / 16; }
|
|
std::array<vr::VROverlayHandle_t, 4> Controls() const { return {bar, curveButton, rollButton, handle}; }
|
|
std::array<vr::VROverlayHandle_t, 5> All() const { return {overlay, bar, curveButton, rollButton, handle}; }
|
|
};
|
|
std::map<int, Screen> g_screens;
|
|
std::map<const void *, vr::SharedTextureHandle_t> g_imports;
|
|
|
|
// Visibility (see the top). g_manual is the hide/show switch: in the always mode it hides
|
|
// the screens, in the others it shows them anyway.
|
|
Mode g_mode = Mode::Always;
|
|
bool g_manual = false;
|
|
double g_wristAngle = 60; // a pinned screen shows while you see its front within this
|
|
double g_gestureAngle = 20; // gesture: look within this of the controller
|
|
std::string g_gestureHand = "left";
|
|
|
|
// ---------------------------------------------------------------- chrome (bar, button, handle)
|
|
|
|
// The controls look like SteamVR's own: a light translucent pill for the bar, dark
|
|
// translucent discs with white glyphs for the buttons (the overlay alpha, kChromeIdle,
|
|
// dims them further until a laser is on them).
|
|
std::vector<uint8_t> PillTexture(int w, int h, uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha) {
|
|
std::vector<uint8_t> px(size_t(w) * h * 4, 0);
|
|
const double r = h / 2.0 - 1;
|
|
for (int y = 0; y < h; ++y)
|
|
for (int x = 0; x < w; ++x) {
|
|
const double cx = std::clamp(double(x), r + 1, w - r - 1), cy = h / 2.0;
|
|
const double d = std::hypot(x + 0.5 - cx, y + 0.5 - cy);
|
|
uint8_t *p = &px[(size_t(y) * w + x) * 4];
|
|
p[0] = red, p[1] = green, p[2] = blue;
|
|
p[3] = uint8_t(std::clamp(r - d + 0.5, 0.0, 1.0) * alpha);
|
|
}
|
|
return px;
|
|
}
|
|
const std::vector<uint8_t> &BarTexture(bool lit) {
|
|
static const auto normal = PillTexture(256, 24, 235, 235, 235, 210), glow = PillTexture(256, 24, 90, 170, 255, 240);
|
|
return lit ? glow : normal;
|
|
}
|
|
|
|
// Paint a control: dark translucent inside `inside(u, v)`, white where `glyph(u, v)`, a
|
|
// faint light rim where `rim(u, v)`. u, v: -1..1 across the texture, v up.
|
|
template <typename In, typename Glyph, typename Rim>
|
|
std::vector<uint8_t> ControlTexture(int n, In inside, Glyph glyph, Rim rim) {
|
|
std::vector<uint8_t> px(size_t(n) * n * 4, 0);
|
|
const int ss = 3; // supersampling, for smooth edges
|
|
for (int y = 0; y < n; ++y)
|
|
for (int x = 0; x < n; ++x) {
|
|
double in = 0, g = 0, e = 0;
|
|
for (int j = 0; j < ss; ++j)
|
|
for (int i = 0; i < ss; ++i) {
|
|
const double u = (x + (i + 0.5) / ss) / n * 2 - 1, v = 1 - (y + (j + 0.5) / ss) / n * 2;
|
|
if (!inside(u, v)) continue;
|
|
in += 1;
|
|
if (glyph(u, v)) g += 1;
|
|
else if (rim(u, v)) e += 1;
|
|
}
|
|
const double k = ss * ss;
|
|
in /= k, g /= k, e /= k;
|
|
uint8_t *p = &px[(size_t(y) * n + x) * 4];
|
|
const double bg = in - g - e; // dark part
|
|
const double a = bg * 0.72 + e * 0.6 + g * 1.0;
|
|
if (a <= 0) continue;
|
|
const double shade = (bg * 0.72 * 38 + e * 0.6 * 200 + g * 255) / a;
|
|
p[0] = p[1] = p[2] = uint8_t(std::clamp(shade, 0.0, 255.0));
|
|
p[3] = uint8_t(std::clamp(a * 255, 0.0, 255.0));
|
|
}
|
|
return px;
|
|
}
|
|
bool InDisc(double u, double v) { return u * u + v * v <= 1; }
|
|
bool DiscRim(double u, double v) { return u * u + v * v > 0.86 * 0.86; }
|
|
|
|
std::vector<uint8_t> CornerTexture(int n) {
|
|
// A quarter disc whose corner (the texture's top left) sits on the screen's bottom
|
|
// right corner, with two grip arcs: "drag this corner".
|
|
auto r = [](double u, double v) { return std::hypot(u + 1, v - 1) / 2; }; // 0..1 from the corner
|
|
return ControlTexture(
|
|
n, [&](double u, double v) { return r(u, v) <= 1; },
|
|
[&](double u, double v) {
|
|
const double d = r(u, v);
|
|
return std::fabs(d - 0.5) < 0.035 || std::fabs(d - 0.75) < 0.035;
|
|
},
|
|
[&](double u, double v) { return r(u, v) > 0.93; });
|
|
}
|
|
|
|
std::vector<uint8_t> CurveTexture(int n) {
|
|
// An arc: "curve this screen".
|
|
return ControlTexture(
|
|
n, InDisc,
|
|
[](double u, double v) { return std::fabs(std::hypot(u, -v - 1.9) - 1.7) < 0.11 && std::fabs(u) < 0.6; },
|
|
DiscRim);
|
|
}
|
|
|
|
std::vector<uint8_t> RollTexture(int n) {
|
|
// A circular arrow, counterclockwise: "roll this screen".
|
|
return ControlTexture(
|
|
n, InDisc,
|
|
[](double u, double v) {
|
|
const double r = std::hypot(u, v);
|
|
double ang = std::atan2(v, u) * 180 / M_PI;
|
|
if (ang < 0) ang += 360;
|
|
if (std::fabs(r - 0.48) < 0.085 && ang >= 100) return true; // the arc, 100..360 degrees
|
|
// The head at 0 degrees, pointing up (the way the arc turns there).
|
|
const double hx = u - 0.48, hy = v + 0.02;
|
|
return hy >= 0 && hy <= 0.3 && std::fabs(hx) <= 0.24 * (1 - hy / 0.3);
|
|
},
|
|
DiscRim);
|
|
}
|
|
|
|
vr::VROverlayHandle_t MakeChrome(const char *key, const char *name, const std::vector<uint8_t> &px, int w, int h) {
|
|
vr::VROverlayHandle_t o = vr::k_ulOverlayHandleInvalid;
|
|
if (vr::VROverlay()->CreateOverlay(key, name, &o) != vr::VROverlayError_None) return o;
|
|
vr::VROverlay()->SetOverlayRaw(o, const_cast<uint8_t *>(px.data()), uint32_t(w), uint32_t(h), 4);
|
|
vr::VROverlay()->SetOverlayInputMethod(o, vr::VROverlayInputMethod_Mouse);
|
|
vr::VROverlay()->SetOverlaySortOrder(o, 10);
|
|
return o;
|
|
}
|
|
|
|
void LightBar(Screen &s, bool lit) {
|
|
if (s.barLit == lit) return;
|
|
s.barLit = lit;
|
|
const auto &px = BarTexture(lit);
|
|
vr::VROverlay()->SetOverlayRaw(s.bar, const_cast<uint8_t *>(px.data()), 256, 24, 4);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- wrist guides
|
|
|
|
// While a screen is carried, each other controller gets a ring (its wrist zone, facing
|
|
// you) and a dot where the laser passes closest to it. Blue: armed / in the ring.
|
|
std::vector<uint8_t> DiscTexture(int n, double stroke, uint8_t red, uint8_t green, uint8_t blue, uint8_t fill,
|
|
uint8_t rimShade) {
|
|
std::vector<uint8_t> px(size_t(n) * n * 4, 0);
|
|
const double c = n / 2.0, r = n / 2.0 - 1;
|
|
for (int y = 0; y < n; ++y)
|
|
for (int x = 0; x < n; ++x) {
|
|
const double d = std::hypot(x + 0.5 - c, y + 0.5 - c);
|
|
const double a = std::clamp(r - d + 0.5, 0.0, 1.0);
|
|
uint8_t *p = &px[(size_t(y) * n + x) * 4];
|
|
const bool rim = d > r - stroke;
|
|
const bool edge = d > r - 2 || (rim && d < r - stroke + 2); // a dark line each side of the rim
|
|
p[0] = edge ? rimShade : red, p[1] = edge ? rimShade : green, p[2] = edge ? rimShade : blue;
|
|
p[3] = uint8_t(a * (rim ? 235 : fill));
|
|
}
|
|
return px;
|
|
}
|
|
const std::vector<uint8_t> &RingTexture(bool lit) {
|
|
static const auto normal = DiscTexture(128, 9, 240, 240, 240, 40, 60),
|
|
glow = DiscTexture(128, 12, 90, 170, 255, 110, 30);
|
|
return lit ? glow : normal;
|
|
}
|
|
const std::vector<uint8_t> &DotTexture(bool lit) {
|
|
static const auto normal = DiscTexture(32, 16, 250, 250, 250, 250, 50),
|
|
glow = DiscTexture(32, 16, 90, 170, 255, 250, 30);
|
|
return lit ? glow : normal;
|
|
}
|
|
|
|
struct GuidePart {
|
|
vr::VROverlayHandle_t overlay = vr::k_ulOverlayHandleInvalid;
|
|
int lit = -1; // the texture on it (-1: none yet)
|
|
bool shown = false;
|
|
void Show(bool on) {
|
|
if (on == shown || overlay == vr::k_ulOverlayHandleInvalid) return;
|
|
shown = on;
|
|
if (on) vr::VROverlay()->ShowOverlay(overlay);
|
|
else vr::VROverlay()->HideOverlay(overlay);
|
|
}
|
|
void Light(bool on, const std::vector<uint8_t> &px, int n) {
|
|
if (int(on) == lit) return;
|
|
lit = on;
|
|
vr::VROverlay()->SetOverlayRaw(overlay, const_cast<uint8_t *>(px.data()), uint32_t(n), uint32_t(n), 4);
|
|
}
|
|
};
|
|
struct Guide { GuidePart ring, dot; };
|
|
std::map<vr::TrackedDeviceIndex_t, Guide> g_guides;
|
|
|
|
Guide &GuideFor(vr::TrackedDeviceIndex_t dev) {
|
|
auto it = g_guides.find(dev);
|
|
if (it != g_guides.end()) return it->second;
|
|
Guide &g = g_guides[dev];
|
|
char key[64];
|
|
std::snprintf(key, sizeof key, "frametop.guide.%u.ring", dev);
|
|
if (vr::VROverlay()->CreateOverlay(key, "Wrist pin target", &g.ring.overlay) == vr::VROverlayError_None) {
|
|
vr::VROverlay()->SetOverlayWidthInMeters(g.ring.overlay, float(2 * kWristZone));
|
|
vr::VROverlay()->SetOverlaySortOrder(g.ring.overlay, 20);
|
|
}
|
|
std::snprintf(key, sizeof key, "frametop.guide.%u.dot", dev);
|
|
if (vr::VROverlay()->CreateOverlay(key, "Wrist pin laser", &g.dot.overlay) == vr::VROverlayError_None) {
|
|
vr::VROverlay()->SetOverlayWidthInMeters(g.dot.overlay, 0.022f);
|
|
vr::VROverlay()->SetOverlaySortOrder(g.dot.overlay, 21);
|
|
}
|
|
return g;
|
|
}
|
|
|
|
// A pose at pt facing the head (upright).
|
|
Mat FacingPose(const double pt[3], const Mat &head) {
|
|
double z[3] = {head.m[0][3] - pt[0], head.m[1][3] - pt[1], head.m[2][3] - pt[2]};
|
|
const double zl = std::sqrt(Dot3(z, z)) + 1e-9;
|
|
for (double &v : z) v /= zl;
|
|
double x[3] = {z[2], 0, -z[0]}; // up x z
|
|
const double xl = std::sqrt(x[0] * x[0] + x[2] * x[2]);
|
|
if (xl < 1e-6) x[0] = 1, x[2] = 0;
|
|
else x[0] /= xl, x[2] /= xl;
|
|
const double y[3] = {z[1] * x[2] - z[2] * x[1], z[2] * x[0] - z[0] * x[2], z[0] * x[1] - z[1] * x[0]};
|
|
Mat m{};
|
|
for (int i = 0; i < 3; ++i) m.m[i][0] = float(x[i]), m.m[i][1] = float(y[i]), m.m[i][2] = float(z[i]), m.m[i][3] = float(pt[i]);
|
|
return m;
|
|
}
|
|
|
|
void ApplyCurve(const Screen &s) {
|
|
// OpenVR's curvature: the fraction of a full cylinder the overlay's width covers.
|
|
const double c = s.curve > 0 ? std::clamp(s.metres / (2 * M_PI * s.curve), 0.0, 1.0) : 0.0;
|
|
vr::VROverlay()->SetOverlayCurvature(s.overlay, float(c));
|
|
}
|
|
|
|
// The screen's pose in the room (a pinned one: its controller's pose times pinRel).
|
|
bool ScreenPose(const Screen &s, Mat *out) {
|
|
if (s.pinned != kNone) {
|
|
Mat d;
|
|
if (!DevicePose(s.pinned, &d)) return false;
|
|
*out = Mul(d, s.pinRel);
|
|
return true;
|
|
}
|
|
vr::ETrackingUniverseOrigin origin;
|
|
return vr::VROverlay()->GetOverlayTransformAbsolute(s.overlay, &origin, out) == vr::VROverlayError_None;
|
|
}
|
|
|
|
// The controls' size from both the screen's width and its distance from the head (the
|
|
// geometric mean of 12% of the width and 10% of the distance), so a small screen near you
|
|
// gets small controls and a big or far one gets big ones, never under about 1.7 degrees.
|
|
void ChromeSize(Screen &s) {
|
|
Mat head, p;
|
|
double dist = 2;
|
|
if (DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head) && ScreenPose(s, &p)) {
|
|
const double d[3] = {p.m[0][3] - head.m[0][3], p.m[1][3] - head.m[1][3], p.m[2][3] - head.m[2][3]};
|
|
dist = std::max(0.2, std::sqrt(Dot3(d, d)));
|
|
}
|
|
const double least = dist * 0.03;
|
|
s.chrome = std::clamp(std::sqrt(0.012 * dist * s.metres), least, std::max(least, s.metres * 0.5));
|
|
s.grip = std::max(s.chrome * 0.13, dist * 0.018);
|
|
}
|
|
|
|
// A point on the screen's surface, u metres along it from the centre (along the arc when
|
|
// curved), v up, dz out of it, facing the way the surface does there. OpenVR curves a
|
|
// screen into a cylinder toward its front, with its centre line where the flat one was.
|
|
Mat OnSurface(const Screen &s, double u, double v, double dz) {
|
|
if (s.curve <= 0) return Translation(u, v, dz);
|
|
const double r = s.curve, a = u / r, c = std::cos(a), sn = std::sin(a);
|
|
Mat m = Identity();
|
|
m.m[0][0] = float(c), m.m[0][2] = float(-sn);
|
|
m.m[2][0] = float(sn), m.m[2][2] = float(c);
|
|
m.m[0][3] = float(r * sn - dz * sn), m.m[1][3] = float(v), m.m[2][3] = float(r - r * c + dz * c);
|
|
return m;
|
|
}
|
|
|
|
double BarY(const Screen &s) { return -(s.heightMetres() / 2 + s.chrome * 0.06 + s.chrome * 12 / 256); }
|
|
Mat BarOffset(const Screen &s) { return OnSurface(s, 0, BarY(s), 0.003); }
|
|
|
|
// Put the bar, the curve button, and the corner tab under the screen (same parent: the
|
|
// room or the controller), sized for the screen and its distance, and on its surface.
|
|
// Where each control sits, relative to the screen: bar, curve, roll, resize tab.
|
|
std::array<Mat, 4> ControlOffsets(const Screen &s) {
|
|
const double h = s.heightMetres(), bar = s.chrome, button = s.grip, gap = bar * 0.06;
|
|
return {BarOffset(s), OnSurface(s, bar / 2 + gap + button / 2, BarY(s), 0.003),
|
|
OnSurface(s, bar / 2 + gap * 2 + button * 1.5, BarY(s), 0.003),
|
|
// The tab's top left corner is the screen's bottom right corner.
|
|
OnSurface(s, s.metres / 2 + s.grip / 2, -(h / 2 + s.grip / 2), 0.003)};
|
|
}
|
|
|
|
void PlaceChrome(Screen &s) {
|
|
ChromeSize(s);
|
|
const double bar = s.chrome, button = s.grip;
|
|
const auto offsets = ControlOffsets(s);
|
|
vr::VROverlay()->SetOverlayWidthInMeters(s.bar, float(bar));
|
|
vr::VROverlay()->SetOverlayWidthInMeters(s.curveButton, float(button));
|
|
vr::VROverlay()->SetOverlayWidthInMeters(s.rollButton, float(button));
|
|
vr::VROverlay()->SetOverlayWidthInMeters(s.handle, float(s.grip));
|
|
// Curved, the bar bends with the screen's bottom edge.
|
|
vr::VROverlay()->SetOverlayCurvature(s.bar, s.curve > 0 ? float(std::min(1.0, bar / (2 * M_PI * s.curve))) : 0.f);
|
|
const std::pair<vr::VROverlayHandle_t, Mat> parts[] = {
|
|
{s.bar, offsets[0]}, {s.curveButton, offsets[1]}, {s.rollButton, offsets[2]}, {s.handle, offsets[3]}};
|
|
if (s.pinned != kNone) {
|
|
for (const auto &[o, off] : parts) {
|
|
const Mat m = Mul(s.pinRel, off);
|
|
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(o, s.pinned, &m);
|
|
}
|
|
return;
|
|
}
|
|
Mat p;
|
|
if (!ScreenPose(s, &p)) return;
|
|
for (const auto &[o, off] : parts) {
|
|
const Mat m = Mul(p, off);
|
|
vr::VROverlay()->SetOverlayTransformAbsolute(o, vr::TrackingUniverseStanding, &m);
|
|
}
|
|
}
|
|
|
|
// Screens you walk up to (or pinned ones you bring close) get their controls resized now
|
|
// and then, not every frame.
|
|
void RefreshChrome() {
|
|
static int tick = 0;
|
|
if (++tick % 45) return;
|
|
for (auto &[i, s] : g_screens) {
|
|
if (s.drag != Drag::None) continue;
|
|
const double before = s.chrome;
|
|
ChromeSize(s);
|
|
if (std::fabs(s.chrome - before) > before * 0.08) PlaceChrome(s);
|
|
else s.chrome = before;
|
|
}
|
|
}
|
|
|
|
void SetAbsolute(Screen &s, const Mat &pose) {
|
|
s.pinned = kNone;
|
|
vr::VROverlay()->SetOverlayTransformAbsolute(s.overlay, vr::TrackingUniverseStanding, &pose);
|
|
PlaceChrome(s);
|
|
}
|
|
|
|
void Pin(Screen &s, vr::TrackedDeviceIndex_t dev, const Mat &rel) {
|
|
s.pinned = dev;
|
|
s.pinRel = rel;
|
|
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(s.overlay, dev, &s.pinRel);
|
|
PlaceChrome(s);
|
|
}
|
|
|
|
void SetWidth(Screen &s, double metres) {
|
|
s.metres = std::clamp(metres, kMinWidth, 12.0);
|
|
vr::VROverlay()->SetOverlayWidthInMeters(s.overlay, float(s.metres));
|
|
ApplyCurve(s); // same radius, so the curvature fraction changes with the width
|
|
PlaceChrome(s);
|
|
}
|
|
|
|
// Curve toward the head: the radius is the head's distance to the screen now.
|
|
void ToggleCurve(Screen &s) {
|
|
Mat head, p;
|
|
if (s.curve > 0 || !DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head) || !ScreenPose(s, &p)) {
|
|
s.curve = 0;
|
|
} else {
|
|
const double dx = p.m[0][3] - head.m[0][3], dy = p.m[1][3] - head.m[1][3], dz = p.m[2][3] - head.m[2][3];
|
|
s.curve = std::max(0.5, std::sqrt(dx * dx + dy * dy + dz * dz));
|
|
}
|
|
ApplyCurve(s);
|
|
PlaceChrome(s);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- visibility
|
|
|
|
// Angle in degrees between a panel's front and the direction from it to the head.
|
|
double FacingAngle(const Mat &p, const Mat &head) {
|
|
double n[3], to[3] = {head.m[0][3] - p.m[0][3], head.m[1][3] - p.m[1][3], head.m[2][3] - p.m[2][3]};
|
|
Column(p, 2, n);
|
|
const double len = std::sqrt(Dot3(to, to)) + 1e-9;
|
|
return std::acos(std::clamp(Dot3(n, to) / len, -1.0, 1.0)) * 180 / M_PI;
|
|
}
|
|
|
|
// The screens' shared visibility for the mode (before a pinned screen's own facing rule).
|
|
bool ModeVisible() {
|
|
switch (g_mode) {
|
|
case Mode::Always: return !g_manual;
|
|
case Mode::Toggle: return g_manual;
|
|
case Mode::Dashboard: return g_manual || vr::VROverlay()->IsDashboardVisible();
|
|
case Mode::Gesture: {
|
|
if (g_manual) return true;
|
|
// Looking at the chosen controller: it's within the gesture angle of the gaze.
|
|
Mat head, c;
|
|
if (!DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head) ||
|
|
!DevicePose(HandDevice(g_gestureHand.c_str()), &c))
|
|
return false;
|
|
double f[3], to[3] = {c.m[0][3] - head.m[0][3], c.m[1][3] - head.m[1][3], c.m[2][3] - head.m[2][3]};
|
|
Column(head, 2, f); // the head's +Z points backward
|
|
const double len = std::sqrt(Dot3(to, to)) + 1e-9;
|
|
return std::acos(std::clamp(-Dot3(f, to) / len, -1.0, 1.0)) * 180 / M_PI <= g_gestureAngle;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// The screen at its alpha; each control dimmer (kChromeIdle) unless a laser is on it or
|
|
// it's being dragged.
|
|
void ApplyAlpha(const Screen &s) {
|
|
vr::VROverlay()->SetOverlayAlpha(s.overlay, s.alpha);
|
|
const bool active[4] = {s.hover[0] || s.drag == Drag::Move, s.hover[1], s.hover[2] || s.drag == Drag::Roll,
|
|
s.hover[3] || s.drag == Drag::Resize};
|
|
const auto controls = s.Controls();
|
|
for (int k = 0; k < 4; ++k)
|
|
vr::VROverlay()->SetOverlayAlpha(controls[k], s.alpha * s.controls * (active[k] ? 1.f : kChromeIdle));
|
|
}
|
|
|
|
void SetVisible(Screen &s, bool visible, float alpha) {
|
|
if (visible && std::fabs(alpha - s.alpha) > 0.01f) {
|
|
s.alpha = alpha;
|
|
ApplyAlpha(s);
|
|
}
|
|
if (visible == s.visible) return;
|
|
s.visible = visible;
|
|
if (visible) {
|
|
vr::VROverlay()->ShowOverlay(s.overlay);
|
|
return;
|
|
}
|
|
// Hidden: the controls go at once (UpdateControls brings them back).
|
|
vr::VROverlay()->HideOverlay(s.overlay);
|
|
for (auto o : s.Controls()) vr::VROverlay()->HideOverlay(o);
|
|
s.controls = 0, s.controlsUp = false;
|
|
}
|
|
|
|
void UpdateVisibility() {
|
|
const bool shared = ModeVisible();
|
|
Mat head;
|
|
const bool haveHead = DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head);
|
|
for (auto &[i, s] : g_screens) {
|
|
bool visible = s.shown && (shared || s.drag != Drag::None);
|
|
float alpha = 1;
|
|
Mat p;
|
|
if (visible && s.pinned != kNone && s.drag == Drag::None && haveHead && ScreenPose(s, &p)) {
|
|
// A pinned screen shows while you see its front: fully inside the wrist angle,
|
|
// fading out over the last kFade degrees, gone beyond it (and from behind).
|
|
const double a = FacingAngle(p, head);
|
|
alpha = float(std::clamp((g_wristAngle - a) / kFade, 0.0, 1.0));
|
|
visible = alpha > 0.02f;
|
|
}
|
|
SetVisible(s, visible, alpha);
|
|
}
|
|
}
|
|
|
|
|
|
// The controls are invisible until a laser passes very close to one of them (within
|
|
// `reach`, about 1.5 times a button's size); they stay kControlsLinger ticks after it
|
|
// leaves, and while in use.
|
|
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))
|
|
lasers.push_back(d);
|
|
}
|
|
for (auto &[i, s] : g_screens) {
|
|
Mat p;
|
|
if (s.visible && ScreenPose(s, &p)) {
|
|
// Points along the bar and at each button and the tab; a laser passing within
|
|
// `reach` of one of them is close.
|
|
std::vector<Mat> spots;
|
|
const auto offsets = ControlOffsets(s);
|
|
for (double f : {-0.5, -0.25, 0.0, 0.25, 0.5})
|
|
spots.push_back(Mul(p, Mul(offsets[0], Translation(f * s.chrome, 0, 0))));
|
|
for (int k = 1; k < 4; ++k) spots.push_back(Mul(p, offsets[k]));
|
|
const double reach = std::max(s.grip * 1.5, s.chrome * 0.12);
|
|
for (const Mat &d : lasers) {
|
|
const double o[3] = {d.m[0][3], d.m[1][3], d.m[2][3]}, dir[3] = {-d.m[0][2], -d.m[1][2], -d.m[2][2]};
|
|
bool close = false;
|
|
for (const Mat &c : spots) {
|
|
const double v[3] = {c.m[0][3] - o[0], c.m[1][3] - o[1], c.m[2][3] - o[2]};
|
|
const double t = Dot3(v, dir);
|
|
if (t <= 0) continue;
|
|
const double q[3] = {v[0] - dir[0] * t, v[1] - dir[1] * t, v[2] - dir[2] * t};
|
|
if (Dot3(q, q) <= reach * reach) close = true;
|
|
}
|
|
if (close) {
|
|
s.nearUntil = g_tick + kControlsLinger;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
const bool inUse = s.drag != Drag::None || s.hover[0] || s.hover[1] || s.hover[2] || s.hover[3];
|
|
const bool want = s.visible && (inUse || g_tick < s.nearUntil);
|
|
const float before = s.controls;
|
|
s.controls = std::clamp(s.controls + (want ? 0.2f : -0.1f), 0.f, 1.f);
|
|
if (s.controls == before) continue;
|
|
if (s.controls > 0 && !s.controlsUp) {
|
|
for (auto o : s.Controls()) vr::VROverlay()->ShowOverlay(o);
|
|
s.controlsUp = true;
|
|
}
|
|
ApplyAlpha(s);
|
|
if (s.controls == 0 && s.controlsUp) {
|
|
for (auto o : s.Controls()) vr::VROverlay()->HideOverlay(o);
|
|
s.controlsUp = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------- moving, resizing, pinning
|
|
|
|
// Where a device's ray meets the screen's plane, in the screen's x (right) and y (up),
|
|
// metres from its centre.
|
|
bool RayOnPlane(const Mat &p, const Mat &d, double *x, double *y) {
|
|
const double o[3] = {d.m[0][3], d.m[1][3], d.m[2][3]}, dir[3] = {-d.m[0][2], -d.m[1][2], -d.m[2][2]};
|
|
const double c[3] = {p.m[0][3], p.m[1][3], p.m[2][3]};
|
|
double n[3], ax[3], ay[3];
|
|
Column(p, 2, n), Column(p, 0, ax), Column(p, 1, ay);
|
|
const double denom = Dot3(dir, n);
|
|
if (std::fabs(denom) < 1e-4) return false;
|
|
const double co[3] = {c[0] - o[0], c[1] - o[1], c[2] - o[2]};
|
|
const double t = Dot3(co, n) / denom;
|
|
if (t <= 0) return false;
|
|
const double rel[3] = {o[0] + dir[0] * t - c[0], o[1] + dir[1] * t - c[1], o[2] + dir[2] * t - c[2]};
|
|
*x = Dot3(rel, ax), *y = Dot3(rel, ay);
|
|
return true;
|
|
}
|
|
bool RayOnScreen(const Screen &s, const Mat &d, double *x, double *y) {
|
|
Mat p;
|
|
return ScreenPose(s, &p) && RayOnPlane(p, d, x, y);
|
|
}
|
|
|
|
// Roll: a rotation about the screen's own front axis (counterclockwise as you see it).
|
|
Mat RollZ(double rad) {
|
|
Mat m = Identity();
|
|
m.m[0][0] = m.m[1][1] = float(std::cos(rad));
|
|
m.m[1][0] = float(std::sin(rad)), m.m[0][1] = float(-std::sin(rad));
|
|
return m;
|
|
}
|
|
|
|
// Roll the screen to `rad` from its pose at the press, snapping level within kRollSnap.
|
|
void ApplyRoll(Screen &s, double rad) {
|
|
const bool pinned = s.pinned != kNone;
|
|
Mat c = Identity();
|
|
if (pinned && !DevicePose(s.pinned, &c)) return;
|
|
const Mat base = pinned ? Mul(c, s.rollFrom) : s.rollFrom;
|
|
const Mat p = Mul(base, RollZ(rad));
|
|
const double tilt = std::asin(std::clamp(double(p.m[1][0]), -1.0, 1.0)); // the right edge's slope
|
|
if (std::fabs(tilt) < kRollSnap * M_PI / 180) rad -= tilt;
|
|
if (pinned) Pin(s, s.pinned, Mul(s.rollFrom, RollZ(rad)));
|
|
else SetAbsolute(s, Mul(base, RollZ(rad)));
|
|
}
|
|
|
|
// The laser's angle around the screen's centre, in the frame of its pose at the press.
|
|
bool RollLaserAngle(const Screen &s, const Mat &d, double *rad) {
|
|
Mat c = Identity();
|
|
if (s.pinned != kNone && !DevicePose(s.pinned, &c)) return false;
|
|
const Mat base = s.pinned != kNone ? Mul(c, s.rollFrom) : s.rollFrom;
|
|
double hx, hy;
|
|
if (!RayOnPlane(base, d, &hx, &hy)) return false;
|
|
*rad = std::atan2(hy, hx);
|
|
return true;
|
|
}
|
|
|
|
// The laser while moving a screen: from the carrying device to the bar.
|
|
void Laser(const Screen &s, const Mat &d, const Mat &p, double a[3], double b[3]) {
|
|
const Mat bar = Mul(p, BarOffset(s));
|
|
for (int k = 0; k < 3; ++k) a[k] = d.m[k][3], b[k] = bar.m[k][3];
|
|
}
|
|
|
|
// The point q on the segment a-b closest to pt, and its distance.
|
|
double SegmentClosest(const double pt[3], const double a[3], const double b[3], double q[3]) {
|
|
const double ab[3] = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}, ap[3] = {pt[0] - a[0], pt[1] - a[1], pt[2] - a[2]};
|
|
const double t = std::clamp(Dot3(ap, ab) / (Dot3(ab, ab) + 1e-12), 0.0, 1.0);
|
|
for (int k = 0; k < 3; ++k) q[k] = a[k] + ab[k] * t;
|
|
const double v[3] = {q[0] - pt[0], q[1] - pt[1], q[2] - pt[2]};
|
|
return std::sqrt(Dot3(v, v));
|
|
}
|
|
double LaserDistance(const Screen &s, const Mat &d, const Mat &p, vr::TrackedDeviceIndex_t dev, double q[3]) {
|
|
Mat c;
|
|
if (!DevicePose(dev, &c)) return 1e9;
|
|
double a[3], b[3];
|
|
Laser(s, d, p, a, b);
|
|
const double pt[3] = {c.m[0][3], c.m[1][3], c.m[2][3]};
|
|
return SegmentClosest(pt, a, b, q);
|
|
}
|
|
|
|
// The hand controller (not the carrying device) whose ring the laser is in, or kNone.
|
|
vr::TrackedDeviceIndex_t WristOnLaser(const Screen &s, const Mat &d, const Mat &p) {
|
|
for (vr::TrackedDeviceIndex_t i = 1; i < vr::k_unMaxTrackedDeviceCount; ++i) {
|
|
double q[3];
|
|
if (i != s.dragDevice && IsHandController(i) && LaserDistance(s, d, p, i, q) <= kWristZone) return i;
|
|
}
|
|
return kNone;
|
|
}
|
|
|
|
void StartDrag(Screen &s, Drag mode, vr::TrackedDeviceIndex_t dev) {
|
|
Mat d, p;
|
|
if (dev == kNone || !DevicePose(dev, &d) || !ScreenPose(s, &p)) return;
|
|
s.pinTarget = kNone;
|
|
if (s.pinned != kNone && mode == Drag::Move) {
|
|
// Carried freely; let go, it goes back on the same wrist (unless disarmed).
|
|
s.pinTarget = s.pinned;
|
|
SetAbsolute(s, p);
|
|
}
|
|
s.drag = mode;
|
|
s.dragDevice = dev;
|
|
s.dragRel = Mul(Inverse(d), p);
|
|
// Already in a ring when grabbed: that doesn't count as crossing it.
|
|
s.onWrist = mode == Drag::Move ? WristOnLaser(s, d, p) : kNone;
|
|
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;
|
|
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;
|
|
}
|
|
ApplyAlpha(s);
|
|
}
|
|
|
|
// Stop moving where it is (a command took over).
|
|
void EndDrag(Screen &s) {
|
|
s.drag = Drag::None;
|
|
s.dragDevice = kNone;
|
|
s.pinTarget = s.onWrist = kNone;
|
|
LightBar(s, false);
|
|
ApplyAlpha(s);
|
|
}
|
|
|
|
// Let go: pin to the armed wrist, as the screen is now.
|
|
void FinishDrag(Screen &s, int index) {
|
|
const bool moved = s.drag == Drag::Move;
|
|
const vr::TrackedDeviceIndex_t target = s.pinTarget;
|
|
EndDrag(s);
|
|
Mat c, p;
|
|
if (!moved) return;
|
|
if (target != kNone && DevicePose(target, &c) && ScreenPose(s, &p)) {
|
|
Pin(s, target, Mul(Inverse(c), p));
|
|
std::printf("screen %d: pinned to the %s controller\n", index + 1, HandName(target));
|
|
}
|
|
}
|
|
|
|
// A button release on any of our panels ends that device's drags (it may be over another
|
|
// screen by then).
|
|
void EndDragsBy(vr::TrackedDeviceIndex_t dev) {
|
|
for (auto &[index, s] : g_screens)
|
|
if (s.drag != Drag::None && s.dragDevice == dev) FinishDrag(s, index);
|
|
}
|
|
|
|
// While moving: the laser entering a controller's ring flips whether the screen pins to
|
|
// it when let go (so sweeping across arms it, sweeping back disarms it).
|
|
void CheckWristAim(Screen &s, const Mat &d, const Mat &p) {
|
|
double q[3];
|
|
if (s.onWrist != kNone && LaserDistance(s, d, p, s.onWrist, q) > kWristLeave) s.onWrist = kNone;
|
|
if (s.onWrist == kNone) {
|
|
s.onWrist = WristOnLaser(s, d, p);
|
|
if (s.onWrist != kNone) s.pinTarget = s.pinTarget == s.onWrist ? kNone : s.onWrist;
|
|
}
|
|
LightBar(s, s.pinTarget != kNone);
|
|
}
|
|
|
|
// Show the rings and dots for the screen being carried (hide them otherwise).
|
|
void UpdateGuides() {
|
|
const Screen *carried = nullptr;
|
|
Mat d, p, head;
|
|
for (auto &[i, s] : g_screens)
|
|
if (s.drag == Drag::Move && DevicePose(s.dragDevice, &d) && ScreenPose(s, &p)) {
|
|
carried = &s;
|
|
break;
|
|
}
|
|
if (!carried || !DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head)) {
|
|
for (auto &[dev, g] : g_guides) g.ring.Show(false), g.dot.Show(false);
|
|
return;
|
|
}
|
|
for (vr::TrackedDeviceIndex_t i = 1; i < vr::k_unMaxTrackedDeviceCount; ++i) {
|
|
Mat c;
|
|
const bool want = i != carried->dragDevice && IsHandController(i) && DevicePose(i, &c);
|
|
if (!want) {
|
|
auto it = g_guides.find(i);
|
|
if (it != g_guides.end()) it->second.ring.Show(false), it->second.dot.Show(false);
|
|
continue;
|
|
}
|
|
Guide &g = GuideFor(i);
|
|
const double pt[3] = {c.m[0][3], c.m[1][3], c.m[2][3]};
|
|
Mat m = FacingPose(pt, head);
|
|
vr::VROverlay()->SetOverlayTransformAbsolute(g.ring.overlay, vr::TrackingUniverseStanding, &m);
|
|
g.ring.Light(carried->pinTarget == i, RingTexture(carried->pinTarget == i), 128);
|
|
g.ring.Show(true);
|
|
double q[3];
|
|
const double dist = LaserDistance(*carried, d, p, i, q);
|
|
if (dist <= kDotRange) {
|
|
m = FacingPose(q, head);
|
|
vr::VROverlay()->SetOverlayTransformAbsolute(g.dot.overlay, vr::TrackingUniverseStanding, &m);
|
|
g.dot.Light(dist <= kWristZone, DotTexture(dist <= kWristZone), 32);
|
|
}
|
|
g.dot.Show(dist <= kDotRange);
|
|
}
|
|
}
|
|
|
|
void UpdateDrag(Screen &s, int index) {
|
|
Mat d;
|
|
if (!DevicePose(s.dragDevice, &d)) return;
|
|
if (s.drag == Drag::Move) {
|
|
const Mat p = Mul(d, s.dragRel);
|
|
SetAbsolute(s, p);
|
|
CheckWristAim(s, d, p);
|
|
return;
|
|
}
|
|
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;
|
|
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;
|
|
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));
|
|
}
|
|
|
|
// Scroll while moving: push the screen away (up) or pull it closer, along the line from
|
|
// the head (not from the carrying device: the 3D mouse's device sits just in front of the
|
|
// bar, below the screen's centre, so that line points mostly up).
|
|
void Push(Screen &s, double notches) {
|
|
Mat d, head;
|
|
if (!DevicePose(s.dragDevice, &d) || !DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head)) return;
|
|
Mat p = Mul(d, s.dragRel);
|
|
const double to[3] = {p.m[0][3] - head.m[0][3], p.m[1][3] - head.m[1][3], p.m[2][3] - head.m[2][3]};
|
|
const double len = std::sqrt(Dot3(to, to));
|
|
const double next = std::clamp(len * (1 + 0.08 * notches), 0.3, 10.0);
|
|
for (int k = 0; k < 3; ++k) p.m[k][3] = float(head.m[k][3] + to[k] / (len + 1e-9) * next);
|
|
s.dragRel = Mul(Inverse(d), p);
|
|
}
|
|
|
|
Screen *Find(int one_based) {
|
|
auto it = g_screens.find(one_based - 1);
|
|
return it == g_screens.end() ? nullptr : &it->second;
|
|
}
|
|
|
|
uint32_t LinuxButton(uint32_t vrButton) {
|
|
switch (vrButton) {
|
|
case vr::VRMouseButton_Right: return BTN_RIGHT;
|
|
case vr::VRMouseButton_Middle: return BTN_MIDDLE;
|
|
default: return BTN_LEFT;
|
|
}
|
|
}
|
|
|
|
const char *ModeName() {
|
|
switch (g_mode) {
|
|
case Mode::Dashboard: return "dashboard";
|
|
case Mode::Gesture: return "gesture";
|
|
case Mode::Toggle: return "toggle";
|
|
default: return "always";
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" {
|
|
|
|
bool ft_vr_init(void) {
|
|
vr::EVRInitError err = vr::VRInitError_None;
|
|
vr::VR_Init(&err, vr::VRApplication_Overlay);
|
|
if (err != vr::VRInitError_None) {
|
|
std::fprintf(stderr, "openvr: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription(err));
|
|
return false;
|
|
}
|
|
if (!vr::VRIPCResourceManager()) {
|
|
std::fprintf(stderr, "openvr: no IVRIPCResourceManagerClient (SteamVR too old?)\n");
|
|
return false;
|
|
}
|
|
RefreshPoses();
|
|
return true;
|
|
}
|
|
|
|
void ft_vr_shutdown(void) {
|
|
for (auto &[i, s] : g_screens)
|
|
for (auto o : s.All()) vr::VROverlay()->DestroyOverlay(o);
|
|
for (auto &[dev, g] : g_guides)
|
|
for (auto o : {g.ring.overlay, g.dot.overlay}) vr::VROverlay()->DestroyOverlay(o);
|
|
for (auto &[k, h] : g_imports) vr::VRIPCResourceManager()->UnrefResource(h);
|
|
g_guides.clear();
|
|
g_screens.clear();
|
|
g_imports.clear();
|
|
vr::VR_Shutdown();
|
|
}
|
|
|
|
int ft_vr_modifiers(uint32_t format, uint64_t *out, int max) {
|
|
uint32_t n = uint32_t(max);
|
|
if (!vr::VRIPCResourceManager()->GetDmabufModifiers(vr::VRApplication_Overlay, format, &n, out)) return 0;
|
|
return int(n < uint32_t(max) ? n : uint32_t(max));
|
|
}
|
|
|
|
bool ft_vr_dashboard_visible(void) { return vr::VROverlay()->IsDashboardVisible(); }
|
|
|
|
void ft_vr_screen_create(int index, double metres, int count) {
|
|
Screen &s = g_screens[index];
|
|
s.metres = metres;
|
|
char key[64], name[64];
|
|
std::snprintf(key, sizeof key, "frametop.screen.%d", index + 1);
|
|
std::snprintf(name, sizeof name, "Screen %d", index + 1);
|
|
if (vr::VROverlay()->CreateOverlay(key, name, &s.overlay) != vr::VROverlayError_None) {
|
|
std::fprintf(stderr, "openvr: can't create overlay %s\n", key);
|
|
return;
|
|
}
|
|
vr::VROverlay()->SetOverlayWidthInMeters(s.overlay, float(metres));
|
|
vr::VROverlay()->SetOverlayInputMethod(s.overlay, vr::VROverlayInputMethod_Mouse);
|
|
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_IgnoreTextureAlpha, true);
|
|
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
|
|
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
|
|
static const auto corner = CornerTexture(64);
|
|
static const auto curve = CurveTexture(64);
|
|
static const auto roll = RollTexture(64);
|
|
std::snprintf(key, sizeof key, "frametop.screen.%d.bar", index + 1);
|
|
std::snprintf(name, sizeof name, "Screen %d: move", index + 1);
|
|
s.bar = MakeChrome(key, name, BarTexture(false), 256, 24);
|
|
vr::VROverlay()->SetOverlayFlag(s.bar, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
|
|
std::snprintf(key, sizeof key, "frametop.screen.%d.curve", index + 1);
|
|
std::snprintf(name, sizeof name, "Screen %d: curve", index + 1);
|
|
s.curveButton = MakeChrome(key, name, curve, 64, 64);
|
|
std::snprintf(key, sizeof key, "frametop.screen.%d.roll", index + 1);
|
|
std::snprintf(name, sizeof name, "Screen %d: roll", index + 1);
|
|
s.rollButton = MakeChrome(key, name, roll, 64, 64);
|
|
vr::VROverlay()->SetOverlayFlag(s.rollButton, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
|
|
std::snprintf(key, sizeof key, "frametop.screen.%d.resize", index + 1);
|
|
std::snprintf(name, sizeof name, "Screen %d: resize", index + 1);
|
|
s.handle = MakeChrome(key, name, corner, 64, 64);
|
|
ApplyAlpha(s);
|
|
// Until the layout places it: 2 m ahead of the head, in a row, screen 1 on the left.
|
|
RefreshPoses();
|
|
Mat head;
|
|
if (!DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head)) head = Identity();
|
|
const double heading = std::atan2(head.m[0][2], head.m[2][2]) * 180 / M_PI;
|
|
const double yaw = heading + (double(count - 1) / 2 - index) * 35;
|
|
const double dx = -std::sin(yaw * M_PI / 180), dz = -std::cos(yaw * M_PI / 180);
|
|
SetAbsolute(s, PanelPose(head.m[0][3] + dx * 2, head.m[1][3], head.m[2][3] + dz * 2, yaw, 0, 0));
|
|
}
|
|
|
|
void ft_vr_screen_destroy(int index) {
|
|
auto it = g_screens.find(index);
|
|
if (it == g_screens.end()) return;
|
|
for (auto o : it->second.All()) vr::VROverlay()->DestroyOverlay(o);
|
|
g_screens.erase(it);
|
|
}
|
|
|
|
bool ft_vr_screen_present(int index, const void *key, const struct ft_dmabuf *b) {
|
|
auto sit = g_screens.find(index);
|
|
if (sit == g_screens.end()) return false;
|
|
Screen &s = sit->second;
|
|
auto it = g_imports.find(key);
|
|
if (it == g_imports.end()) {
|
|
vr::DmabufAttributes_t a{};
|
|
a.unWidth = uint32_t(b->width);
|
|
a.unHeight = uint32_t(b->height);
|
|
a.unDepth = a.unMipLevels = a.unArrayLayers = a.unSampleCount = 1;
|
|
a.unFormat = b->format;
|
|
a.ulModifier = b->modifier;
|
|
a.unPlaneCount = uint32_t(b->n_planes);
|
|
for (int i = 0; i < b->n_planes && i < int(vr::MaxDmabufPlaneCount); ++i) {
|
|
a.plane[i].unOffset = b->offset[i];
|
|
a.plane[i].unStride = b->stride[i];
|
|
a.plane[i].nFd = b->fd[i];
|
|
}
|
|
vr::SharedTextureHandle_t h = 0;
|
|
if (!vr::VRIPCResourceManager()->ImportDmabuf(vr::VRApplication_Overlay, &a, &h)) {
|
|
std::fprintf(stderr, "openvr: ImportDmabuf failed: %dx%d format 0x%x modifier 0x%llx\n", b->width,
|
|
b->height, b->format, (unsigned long long)b->modifier);
|
|
return false;
|
|
}
|
|
it = g_imports.emplace(key, h).first;
|
|
}
|
|
if (b->width != s.width || b->height != s.height) {
|
|
s.width = b->width, s.height = b->height;
|
|
vr::HmdVector2_t scale = {float(s.width), float(s.height)};
|
|
vr::VROverlay()->SetOverlayMouseScale(s.overlay, &scale);
|
|
PlaceChrome(s); // the height changed
|
|
std::printf("screen %d: %dx%d\n", index + 1, s.width, s.height);
|
|
}
|
|
vr::SharedTextureHandle_t handle = it->second;
|
|
vr::Texture_t tex = {&handle, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma};
|
|
vr::VROverlay()->SetOverlayTexture(s.overlay, &tex);
|
|
s.shown = key; // UpdateVisibility shows it on the next tick
|
|
return true;
|
|
}
|
|
|
|
void ft_vr_forget(const void *key) {
|
|
auto it = g_imports.find(key);
|
|
if (it == g_imports.end()) return;
|
|
vr::VRIPCResourceManager()->UnrefResource(it->second);
|
|
g_imports.erase(it);
|
|
}
|
|
|
|
void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
|
|
RefreshPoses();
|
|
for (auto &[index, s] : g_screens) {
|
|
vr::VREvent_t ev;
|
|
// The screen itself: input for KWin.
|
|
while (vr::VROverlay()->PollNextOverlayEvent(s.overlay, &ev, sizeof ev)) {
|
|
ft_event e{};
|
|
e.screen = index;
|
|
switch (ev.eventType) {
|
|
case vr::VREvent_MouseMove:
|
|
e.type = FT_MOTION;
|
|
e.x = ev.data.mouse.x;
|
|
e.y = s.height - ev.data.mouse.y; // OpenVR's mouse origin is bottom left
|
|
break;
|
|
case vr::VREvent_MouseButtonDown:
|
|
case vr::VREvent_MouseButtonUp:
|
|
if (ev.eventType == vr::VREvent_MouseButtonUp) EndDragsBy(ev.trackedDeviceIndex);
|
|
e.type = FT_BUTTON;
|
|
e.button = LinuxButton(ev.data.mouse.button);
|
|
e.pressed = ev.eventType == vr::VREvent_MouseButtonDown;
|
|
e.x = ev.data.mouse.x;
|
|
e.y = s.height - ev.data.mouse.y;
|
|
break;
|
|
case vr::VREvent_ScrollDiscrete:
|
|
e.type = FT_SCROLL;
|
|
e.dx = -ev.data.scroll.xdelta;
|
|
e.dy = -ev.data.scroll.ydelta;
|
|
break;
|
|
case vr::VREvent_FocusLeave:
|
|
e.type = FT_LEAVE;
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
handle(&e, data);
|
|
}
|
|
// The controls light up under a laser.
|
|
auto hover = [&](int k) {
|
|
const bool on = ev.eventType == vr::VREvent_MouseMove || ev.eventType == vr::VREvent_FocusEnter;
|
|
if (!on && ev.eventType != vr::VREvent_FocusLeave) return;
|
|
if (s.hover[k] != on) s.hover[k] = on, ApplyAlpha(s);
|
|
};
|
|
// The bar: move (and push/pull with the wheel while moving).
|
|
while (vr::VROverlay()->PollNextOverlayEvent(s.bar, &ev, sizeof ev)) {
|
|
hover(0);
|
|
if (ev.eventType == vr::VREvent_MouseButtonDown && ev.data.mouse.button == vr::VRMouseButton_Left)
|
|
StartDrag(s, Drag::Move, ev.trackedDeviceIndex);
|
|
else if (ev.eventType == vr::VREvent_MouseButtonUp)
|
|
EndDragsBy(ev.trackedDeviceIndex);
|
|
else if (ev.eventType == vr::VREvent_ScrollDiscrete && s.drag == Drag::Move)
|
|
Push(s, ev.data.scroll.ydelta);
|
|
}
|
|
// The corner: resize.
|
|
while (vr::VROverlay()->PollNextOverlayEvent(s.handle, &ev, sizeof ev)) {
|
|
hover(3);
|
|
if (ev.eventType == vr::VREvent_MouseButtonDown && ev.data.mouse.button == vr::VRMouseButton_Left)
|
|
StartDrag(s, Drag::Resize, ev.trackedDeviceIndex);
|
|
else if (ev.eventType == vr::VREvent_MouseButtonUp)
|
|
EndDragsBy(ev.trackedDeviceIndex);
|
|
}
|
|
// The curve button.
|
|
while (vr::VROverlay()->PollNextOverlayEvent(s.curveButton, &ev, sizeof ev)) {
|
|
hover(1);
|
|
if (ev.eventType == vr::VREvent_MouseButtonDown && ev.data.mouse.button == vr::VRMouseButton_Left)
|
|
ToggleCurve(s);
|
|
else if (ev.eventType == vr::VREvent_MouseButtonUp)
|
|
EndDragsBy(ev.trackedDeviceIndex);
|
|
}
|
|
// The roll button: drag around like a knob, or scroll.
|
|
while (vr::VROverlay()->PollNextOverlayEvent(s.rollButton, &ev, sizeof ev)) {
|
|
hover(2);
|
|
if (ev.eventType == vr::VREvent_MouseButtonDown && ev.data.mouse.button == vr::VRMouseButton_Left)
|
|
StartDrag(s, Drag::Roll, ev.trackedDeviceIndex);
|
|
else if (ev.eventType == vr::VREvent_MouseButtonUp)
|
|
EndDragsBy(ev.trackedDeviceIndex);
|
|
else if (ev.eventType == vr::VREvent_ScrollDiscrete && s.drag == Drag::None) {
|
|
Mat p;
|
|
if (!ScreenPose(s, &p)) continue;
|
|
s.rollFrom = s.pinned != kNone ? s.pinRel : p;
|
|
ApplyRoll(s, ev.data.scroll.ydelta * kRollStep * M_PI / 180);
|
|
}
|
|
}
|
|
if (s.drag != Drag::None) UpdateDrag(s, index);
|
|
}
|
|
RefreshChrome();
|
|
vr::VREvent_t ev;
|
|
while (vr::VRSystem()->PollNextEvent(&ev, sizeof ev)) {
|
|
if (ev.eventType == vr::VREvent_Quit) {
|
|
ft_event e{};
|
|
e.type = FT_QUIT;
|
|
handle(&e, data);
|
|
}
|
|
// A carrying controller that goes away drops its screen.
|
|
if (ev.eventType == vr::VREvent_TrackedDeviceDeactivated) EndDragsBy(ev.trackedDeviceIndex);
|
|
}
|
|
++g_tick;
|
|
UpdateVisibility();
|
|
UpdateControls();
|
|
UpdateGuides();
|
|
}
|
|
|
|
// Control commands (datagrams on @ft_screens, replies to the sender):
|
|
// place <screen> <x> <y> <z> <yaw> <pitch> <roll> centre (standing universe) and facing
|
|
// width <screen> <metres>
|
|
// curve <screen> <radius> cylinder radius in metres; 0 = flat
|
|
// curve <screen> on|off on: the radius is the head's distance to it now -> "ok <radius>"
|
|
// pin <screen|all> <left|right> [12 numbers] pin to that hand's controller: as it is now,
|
|
// or at the given controller->screen transform (rows of a 3x4)
|
|
// unpin <screen|all>
|
|
// get <screen> -> "ok x y z xx xy xz yx yy yz zx zy zz width height curve hand
|
|
// [12 numbers: controller->screen, when pinned]"
|
|
// screens -> "ok <count> <index>:<pixels w>x<h>:<metres> ..."
|
|
// head -> "ok x y z yaw"
|
|
// visibility always|dashboard|gesture|toggle
|
|
// wrist <degrees> a pinned screen shows while you see its front within this
|
|
// gesture <left|right> <degrees> the gesture mode: look within this of that controller
|
|
// hide | show | toggle the manual switch (see g_manual)
|
|
// state -> "ok <mode> <manual 0|1> <wrist deg> <gesture hand> <gesture deg>"
|
|
// (size <screen> <w> <h> and key <code> <value> are handled in compositor.c.) Screens are
|
|
// numbered from 1 here, like everywhere the user sees them.
|
|
void ft_vr_command(const char *cmd, char *reply, int size) {
|
|
RefreshPoses();
|
|
int n;
|
|
double x, y, z, yaw, pitch, roll, w;
|
|
char word[16], hand[16];
|
|
float r[12];
|
|
auto each = [&](const char *which, auto fn) -> bool { // "all" or a screen number
|
|
if (std::strcmp(which, "all") == 0) {
|
|
for (auto &[i, s] : g_screens) fn(s);
|
|
return true;
|
|
}
|
|
Screen *s = Find(std::atoi(which));
|
|
if (s) fn(*s);
|
|
return s != nullptr;
|
|
};
|
|
if (std::sscanf(cmd, "place %d %lf %lf %lf %lf %lf %lf", &n, &x, &y, &z, &yaw, &pitch, &roll) == 7) {
|
|
Screen *s = Find(n);
|
|
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
|
|
EndDrag(*s);
|
|
SetAbsolute(*s, PanelPose(x, y, z, yaw, pitch, roll));
|
|
std::snprintf(reply, size, "ok");
|
|
} else if (std::sscanf(cmd, "width %d %lf", &n, &w) == 2) {
|
|
Screen *s = Find(n);
|
|
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
|
|
SetWidth(*s, w);
|
|
std::snprintf(reply, size, "ok");
|
|
} else if (std::sscanf(cmd, "curve %d %7s", &n, word) == 2 && (!std::strcmp(word, "on") || !std::strcmp(word, "off"))) {
|
|
Screen *s = Find(n);
|
|
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
|
|
if ((s->curve > 0) != (word[1] == 'n')) ToggleCurve(*s);
|
|
std::snprintf(reply, size, "ok %.3f", s->curve);
|
|
} else if (std::sscanf(cmd, "curve %d %lf", &n, &w) == 2) {
|
|
Screen *s = Find(n);
|
|
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
|
|
s->curve = w > 0 ? std::max(0.5, w) : 0;
|
|
ApplyCurve(*s);
|
|
PlaceChrome(*s);
|
|
std::snprintf(reply, size, "ok");
|
|
} else if (const int got = std::sscanf(cmd, "pin %15s %15s %f %f %f %f %f %f %f %f %f %f %f %f", word, hand,
|
|
&r[0], &r[1], &r[2], &r[3], &r[4], &r[5], &r[6], &r[7], &r[8], &r[9],
|
|
&r[10], &r[11]);
|
|
got >= 2) {
|
|
const vr::TrackedDeviceIndex_t dev = HandDevice(hand);
|
|
Mat c;
|
|
if (dev == kNone || !DevicePose(dev, &c))
|
|
return (void)std::snprintf(reply, size, "error no %s controller tracked", hand);
|
|
Mat rel = Identity();
|
|
for (int k = 0; k < 12; ++k) rel.m[k / 4][k % 4] = r[k];
|
|
const bool found = each(word, [&](Screen &s) {
|
|
Mat p;
|
|
EndDrag(s);
|
|
if (got == 14) Pin(s, dev, rel);
|
|
else if (ScreenPose(s, &p)) Pin(s, dev, Mul(Inverse(c), p));
|
|
});
|
|
std::snprintf(reply, size, found ? "ok" : "error no such screen");
|
|
} else if (std::sscanf(cmd, "unpin %15s", word) == 1) {
|
|
const bool found = each(word, [&](Screen &s) {
|
|
Mat p;
|
|
if (s.pinned != kNone && ScreenPose(s, &p)) SetAbsolute(s, p);
|
|
});
|
|
std::snprintf(reply, size, found ? "ok" : "error no such screen");
|
|
} else if (std::sscanf(cmd, "get %d", &n) == 1) {
|
|
Screen *s = Find(n);
|
|
Mat m;
|
|
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
|
|
if (!ScreenPose(*s, &m)) return (void)std::snprintf(reply, size, "error screen %d has no pose", n);
|
|
int len = std::snprintf(reply, size,
|
|
"ok %.4f %.4f %.4f %.5f %.5f %.5f %.5f %.5f %.5f %.5f %.5f %.5f %.4f %.4f %.3f %s",
|
|
m.m[0][3], m.m[1][3], m.m[2][3], m.m[0][0], m.m[1][0], m.m[2][0], m.m[0][1], m.m[1][1],
|
|
m.m[2][1], m.m[0][2], m.m[1][2], m.m[2][2], s->metres, s->heightMetres(), s->curve,
|
|
s->pinned == kNone ? "none" : HandName(s->pinned));
|
|
if (s->pinned != kNone)
|
|
for (int k = 0; k < 12 && len < size; ++k)
|
|
len += std::snprintf(reply + len, size - len, " %.5f", s->pinRel.m[k / 4][k % 4]);
|
|
} else if (std::strncmp(cmd, "screens", 7) == 0) {
|
|
int len = std::snprintf(reply, size, "ok %zu", g_screens.size());
|
|
for (auto &[i, s] : g_screens)
|
|
if (len < size)
|
|
len += std::snprintf(reply + len, size - len, " %d:%dx%d:%.3f", i + 1, s.width, s.height, s.metres);
|
|
} else if (std::strncmp(cmd, "head", 4) == 0) {
|
|
Mat m;
|
|
if (!DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &m))
|
|
return (void)std::snprintf(reply, size, "error no head pose (headset off?)");
|
|
std::snprintf(reply, size, "ok %.4f %.4f %.4f %.2f", m.m[0][3], m.m[1][3], m.m[2][3],
|
|
std::atan2(m.m[0][2], m.m[2][2]) * 180 / M_PI);
|
|
} else if (std::sscanf(cmd, "visibility %15s", word) == 1) {
|
|
const std::string m = word;
|
|
if (m == "always") g_mode = Mode::Always;
|
|
else if (m == "dashboard") g_mode = Mode::Dashboard;
|
|
else if (m == "gesture") g_mode = Mode::Gesture;
|
|
else if (m == "toggle") g_mode = Mode::Toggle;
|
|
else return (void)std::snprintf(reply, size, "error modes: always dashboard gesture toggle");
|
|
g_manual = false;
|
|
std::snprintf(reply, size, "ok %s", ModeName());
|
|
} else if (std::sscanf(cmd, "wrist %lf", &w) == 1) {
|
|
g_wristAngle = std::clamp(w, 10.0, 180.0);
|
|
std::snprintf(reply, size, "ok");
|
|
} else if (std::sscanf(cmd, "gesture %15s %lf", hand, &w) == 2) {
|
|
g_gestureHand = std::strcmp(hand, "right") == 0 ? "right" : "left";
|
|
g_gestureAngle = std::clamp(w, 5.0, 90.0);
|
|
std::snprintf(reply, size, "ok");
|
|
} else if (!std::strncmp(cmd, "hide", 4) || !std::strncmp(cmd, "show", 4) || !std::strncmp(cmd, "toggle", 6)) {
|
|
const bool shownNow = g_mode == Mode::Always ? !g_manual : g_manual;
|
|
const bool want = cmd[0] == 's' ? true : cmd[0] == 'h' ? false : !shownNow;
|
|
g_manual = g_mode == Mode::Always ? !want : want;
|
|
UpdateVisibility();
|
|
std::snprintf(reply, size, "ok %s", want ? "shown" : "hidden");
|
|
} else if (std::strncmp(cmd, "state", 5) == 0) {
|
|
std::snprintf(reply, size, "ok %s %d %.0f %s %.0f", ModeName(), g_manual ? 1 : 0, g_wristAngle,
|
|
g_gestureHand.c_str(), g_gestureAngle);
|
|
} else {
|
|
std::snprintf(reply, size, "error unknown command");
|
|
}
|
|
}
|
|
|
|
} // extern "C"
|