Files
Toastconcern--FrameStudio/src/panel_dock.h
T
ToastconcernandClaude Opus 5.5 f7345a5ac3 Frame Studio and Frame Home: cross-platform builds
Frame Studio now builds for Windows (x64, x86, ARM64), Linux (x64, x86, ARM64)
and macOS (Apple Silicon, Intel):
- studio/platform.h with Win32 and POSIX implementations (dialogs through
  tinyfiledialogs on Linux/macOS), a GLFW window beside the Win32 one, and a
  platform-neutral frame pacer
- MoltenVK support in the renderer (portability enumeration) and bundled in
  the macOS app
- 32-bit builds: large-address-aware, LZMA packing within a 32-bit address space
- unaligned file reads in the loaders go through loadUnaligned (no UB)
- scripts/build-studio.sh, studio/build_windows.bat [x64|x86|arm64],
  cmake/llvm-mingw.cmake, and a GitHub Actions workflow building every target

Frame Home installs without Studio: frame/install-frame-home.{sh,ps1} and
"Install Frame Home.bat" (packaged as FrameHome-installer.zip).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:03:25 -05:00

178 lines
8.9 KiB
C++

#pragma once
// Wall spots at work: a SteamVR panel (a desktop or game window, any free-floating overlay) let go of near one of
// the home's wall spots docks onto it — flush with the wall, facing the room, no wider than the spot — and stays
// there as you walk and teleport (panels live in tracking space, so a docked one is re-placed from the home's frame
// whenever you move). Grab it away and it's free again, back at its own size. Docked panels are re-placed only when
// the rig moves: setting one every frame would overwrite a grab before it could be seen.
// Meta's homes mark these spots with WallPlacementComponent; Frame Studio edits them (World > Wall Spot).
#include "core/world.h"
#include "overlays.h"
#include <openvr.h>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <map>
#include <mutex>
#include <thread>
#include <vector>
class PanelDock {
public:
~PanelDock() { stop(); }
// Before VR_Shutdown: the scanner calls into SteamVR's client library, which shutting down unloads.
void stop() { stop_ = true; if (scanner_.joinable()) scanner_.join(); }
void setWorld(const HomeWorld& w) {
undockAll();
spots_ = w.wallSpots;
if (!spots_.empty() && !scanner_.joinable()) scanner_ = std::thread([this] { scanLoop(); });
}
// Once per frame with worldFromTracking.
void update(const float rig[16]) {
if (spots_.empty()) return;
std::vector<OverlayInfo> panels;
{ std::lock_guard<std::mutex> l(mutex_); panels = panels_; }
float inv[16];
invertRigid(rig, inv);
const auto now = std::chrono::steady_clock::now();
vr::IVROverlay* ov = vr::VROverlay();
// Panels whose app went away: their state (and any spot they held) goes too.
for (auto it = state_.begin(); it != state_.end();) {
bool present = false;
for (const OverlayInfo& p : panels) present |= p.handle == it->first;
it = present ? std::next(it) : state_.erase(it);
}
for (const OverlayInfo& p : panels) {
if (!ov->IsOverlayVisible(p.handle)) continue;
vr::VROverlayTransformType tt;
if (ov->GetOverlayTransformType(p.handle, &tt) != vr::VROverlayError_None || tt != vr::VROverlayTransform_Absolute) continue;
vr::ETrackingUniverseOrigin origin;
vr::HmdMatrix34_t cur;
if (ov->GetOverlayTransformAbsolute(p.handle, &origin, &cur) != vr::VROverlayError_None) continue;
if (origin != vr::TrackingUniverseStanding) continue; // the spots are placed in standing space
State& s = state_[p.handle];
if (s.spot >= 0) {
// Docked: still where we put it? Then keep it on the wall as the rig moves; else it was grabbed away.
if (differs(cur, s.placed, 0.03f)) { undock(p.handle, s, "moved away"); s.still = now; s.last = cur; continue; }
// Re-placed only once the rig has moved a few millimetres since it was placed (per panel, so one
// hidden while you teleported is put right when it shows again). Tiny continuous rig changes —
// seated eye-height easing, step easing — would otherwise overwrite a grab every frame.
float moved = 0;
for (int k = 0; k < 16; ++k) moved = std::max(moved, std::fabs(rig[k] - s.rig[k]));
if (moved < 0.005f) continue;
vr::HmdMatrix34_t m = spotInTracking(s.spot, inv, s.flip);
ov->SetOverlayTransformAbsolute(p.handle, origin, &m);
s.placed = m;
std::memcpy(s.rig, rig, sizeof s.rig);
continue;
}
if (differs(cur, s.last, 0.002f)) { s.last = cur; s.still = now; continue; } // still moving
if (now - s.still < std::chrono::milliseconds(500) || s.cooldown > now) continue;
// At rest: dock onto the nearest free spot within 0.6 m.
float pt[3];
for (int k = 0; k < 3; ++k) pt[k] = rig[k] * cur.m[0][3] + rig[4 + k] * cur.m[1][3] + rig[8 + k] * cur.m[2][3] + rig[12 + k];
int best = -1;
float bestD = 0.6f;
for (size_t i = 0; i < spots_.size(); ++i) {
if (taken(static_cast<int>(i))) continue;
const float* c = &spots_[i].world[12];
const float d = std::sqrt((pt[0] - c[0]) * (pt[0] - c[0]) + (pt[1] - c[1]) * (pt[1] - c[1]) + (pt[2] - c[2]) * (pt[2] - c[2]));
if (d < bestD) { bestD = d; best = static_cast<int>(i); }
}
if (best < 0) { s.cooldown = now + std::chrono::milliseconds(500); continue; }
// Face the panel the way it was facing (towards the user), whichever way the spot's frame points.
const float* z = &spots_[best].world[8];
float pz[3];
for (int k = 0; k < 3; ++k) pz[k] = rig[k] * cur.m[0][2] + rig[4 + k] * cur.m[1][2] + rig[8 + k] * cur.m[2][2];
s.flip = z[0] * pz[0] + z[1] * pz[1] + z[2] * pz[2] < 0;
ov->GetOverlayWidthInMeters(p.handle, &s.width);
const float fit = std::min(s.width, spots_[best].width);
ov->SetOverlayWidthInMeters(p.handle, fit);
vr::HmdMatrix34_t m = spotInTracking(best, inv, s.flip);
ov->SetOverlayTransformAbsolute(p.handle, origin, &m);
s.placed = m;
std::memcpy(s.rig, rig, sizeof s.rig);
s.spot = best;
std::fprintf(stderr, "frame-home: docked panel %s on wall spot %s\n", p.key.c_str(), spots_[best].name.c_str());
}
}
private:
struct State {
int spot = -1;
bool flip = false;
float width = 1; // its own width, given back when it leaves the wall
vr::HmdMatrix34_t placed = {}, last = {};
float rig[16] = {}; // the rig it was last placed for
std::chrono::steady_clock::time_point still{}, cooldown{};
};
std::vector<WorldWallSpot> spots_;
std::map<vr::VROverlayHandle_t, State> state_;
std::vector<OverlayInfo> panels_;
std::mutex mutex_;
std::thread scanner_;
std::atomic<bool> stop_{false};
// The panel list, refreshed every 2 s off the frame loop (a sweep takes ~0.1 s).
void scanLoop() {
while (!stop_) {
std::vector<OverlayInfo> found;
for (const OverlayInfo& o : listOverlays())
if (isUserPanel(o) && o.key.rfind("system.", 0) != 0) found.push_back(o);
{ std::lock_guard<std::mutex> l(mutex_); panels_ = std::move(found); }
for (int i = 0; i < 20 && !stop_; ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
bool taken(int spot) const {
for (const auto& kv : state_) if (kv.second.spot == spot) return true;
return false;
}
void undock(vr::VROverlayHandle_t h, State& s, const char* why) {
vr::VROverlay()->SetOverlayWidthInMeters(h, s.width);
std::fprintf(stderr, "frame-home: panel left wall spot %s (%s)\n", spots_[s.spot].name.c_str(), why);
s.spot = -1;
s.cooldown = std::chrono::steady_clock::now() + std::chrono::seconds(2);
}
void undockAll() {
for (auto& kv : state_) if (kv.second.spot >= 0) undock(kv.first, kv.second, "new home");
state_.clear();
}
// The spot's frame in tracking space, 1 cm off the wall, optionally turned to face the other way.
vr::HmdMatrix34_t spotInTracking(int i, const float inv[16], bool flip) const {
float w[16];
std::memcpy(w, spots_[i].world, 64);
for (int c = 0; c < 3; ++c) { // unit axes (a scaled spot frame would scale the panel)
const float L = std::sqrt(w[c * 4] * w[c * 4] + w[c * 4 + 1] * w[c * 4 + 1] + w[c * 4 + 2] * w[c * 4 + 2]);
if (L > 1e-6f) for (int r = 0; r < 3; ++r) w[c * 4 + r] /= L;
}
if (flip) for (int r = 0; r < 3; ++r) { w[r] = -w[r]; w[8 + r] = -w[8 + r]; } // 180 degrees about Y
for (int r = 0; r < 3; ++r) w[12 + r] += w[8 + r] * 0.01f;
float t[16];
for (int c = 0; c < 4; ++c)
for (int r = 0; r < 4; ++r) t[c * 4 + r] = inv[r] * w[c * 4] + inv[4 + r] * w[c * 4 + 1] + inv[8 + r] * w[c * 4 + 2] + inv[12 + r] * w[c * 4 + 3];
vr::HmdMatrix34_t m;
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) m.m[r][c] = t[c * 4 + r];
return m;
}
static void invertRigid(const float m[16], float o[16]) {
for (int r = 0; r < 3; ++r) for (int c = 0; c < 3; ++c) o[c * 4 + r] = m[r * 4 + c];
for (int r = 0; r < 3; ++r) o[12 + r] = -(o[r] * m[12] + o[4 + r] * m[13] + o[8 + r] * m[14]);
o[3] = o[7] = o[11] = 0; o[15] = 1;
}
static bool differs(const vr::HmdMatrix34_t& a, const vr::HmdMatrix34_t& b, float eps) {
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) if (std::fabs(a.m[r][c] - b.m[r][c]) > eps) return true;
return false;
}
};