Add session-only anchored overlay resize grip

This commit is contained in:
baketnk committed 2026-09-24 15:50:55 -04:00
1 parent 468d3e9b63
commit 19a3db7667
9 files changed
+93 -10

No files matched your search

+2
View File
@@ -29,6 +29,8 @@ See [third-party notes](docs/third-party.md). No GitHub release is published yet
Nothing inserts or submits automatically after transcription. Nothing inserts or submits automatically after transcription.
- **Overlay:** Record/Stop, Cancel, paginated preview, Insert, Enter and Quit. - **Overlay:** Record/Stop, Cancel, paginated preview, Insert, Enter and Quit.
Review and settings share one Inconsolata/neon-framed surface. Review and settings share one Inconsolata/neon-framed surface.
Drag the inset lower-right grip to resize the panel (world, head or wrist);
the upper-left corner stays anchored. Size changes last for this run only.
**Bindings** requests SteamVR's binding editor directly. Existing SteamVR overrides may supersede defaults. World-space by **Bindings** requests SteamVR's binding editor directly. Existing SteamVR overrides may supersede defaults. World-space by
default; settings offer left wrist, right wrist and head mounting, plus recenter. default; settings offer left wrist, right wrist and head mounting, plus recenter.
Dashboard lasers provide clickable controls. Settings → Lasers anytime is an Dashboard lasers provide clickable controls. Settings → Lasers anytime is an
+7 -1
View File
@@ -181,7 +181,13 @@ 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. 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. 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. The left wrist uses World/head width is 0.85 m; wrist width defaults to 0.30 m. All mounts have
the same lower-right resize grip: drag it to scale the panel
between half and twice its configured width. The original upper-left corner
remains fixed relative to the chosen mount, rather than scaling about the
center. The physical resize is session-only; switching mounts retains the
scale factor, while restart restores configured defaults. Grip behavior and
comfort still need an opt-in on-headset check. The left wrist uses
VR Workspace's fallback watch-face axes: panel-right points toward the fingers 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), (controller -Z), panel-up points out of the back of the hand (controller +Y),
and panel-front points toward controller +X. The right wrist reverses panel-right and panel-front points toward controller +X. The right wrist reverses panel-right
+8
View File
@@ -95,6 +95,14 @@ float mount_width(Mount mount, const WristPlacement& wrist) {
return mount == Mount::LeftWrist || mount == Mount::RightWrist ? wrist.width : 0.85f; return mount == Mount::LeftWrist || mount == Mount::RightWrist ? wrist.width : 0.85f;
} }
Matrix34 resized_mount_pose(Matrix34 pose, float original_width, float scale, float aspect) {
const float dx = original_width * (scale - 1.f) * .5f;
const float dy = -dx * aspect;
for (int row = 0; row < 3; ++row)
pose[row][3] += pose[row][0] * dx + pose[row][1] * dy;
return pose;
}
std::filesystem::path default_mount_settings_path() { std::filesystem::path default_mount_settings_path() {
try { try {
const char* xdg = std::getenv("XDG_CONFIG_HOME"); const char* xdg = std::getenv("XDG_CONFIG_HOME");
+3
View File
@@ -29,6 +29,9 @@ std::optional<Matrix34> world_mount_pose(const Matrix34& hmd);
// Device-relative transform; World has no relative anchor and returns identity. // Device-relative transform; World has no relative anchor and returns identity.
Matrix34 relative_mount_pose(Mount mount, const WristPlacement& wrist = {}); Matrix34 relative_mount_pose(Mount mount, const WristPlacement& wrist = {});
float mount_width(Mount mount, const WristPlacement& wrist = {}); float mount_width(Mount mount, const WristPlacement& wrist = {});
// Move the center so the upper-left corner of the original panel remains
// anchored when the same-aspect-ratio canvas changes physical width.
Matrix34 resized_mount_pose(Matrix34 pose, float original_width, float scale, float aspect);
// Empty when neither XDG_CONFIG_HOME nor HOME specifies an absolute base. // Empty when neither XDG_CONFIG_HOME nor HOME specifies an absolute base.
std::filesystem::path default_mount_settings_path(); std::filesystem::path default_mount_settings_path();
+20 -5
View File
@@ -76,6 +76,8 @@ struct Overlay::Impl {
Panel panel; Panel panel;
bool save_failed = false, debug_save_failed = false; bool save_failed = false, debug_save_failed = false;
bool world_ready = false, placed = false, has_texture = false, shown = false; bool world_ready = false, placed = false, has_texture = false, shown = false;
float size_scale = 1.f;
bool size_changed = false;
vr::HmdMatrix34_t world_transform{}; vr::HmdMatrix34_t world_transform{};
std::optional<Mount> applied_mount; std::optional<Mount> applied_mount;
vr::TrackedDeviceIndex_t anchor = vr::k_unTrackedDeviceIndexInvalid; vr::TrackedDeviceIndex_t anchor = vr::k_unTrackedDeviceIndexInvalid;
@@ -211,21 +213,30 @@ struct Overlay::Impl {
applied_mount.reset(); applied_mount.reset();
} }
surface.set_placement_note(note); surface.set_placement_note(note);
if (applied_mount != effective || (effective != Mount::World && anchor != target)) { if (applied_mount != effective || (effective != Mount::World && anchor != target) || size_changed) {
if (!size_changed || applied_mount != effective || anchor != target)
surface.reset_pointers(); // a release on a relocated surface cannot activate an old press surface.reset_pointers(); // a release on a relocated surface cannot activate an old press
const float base_width = mount_width(effective, config.wrist);
if (effective == Mount::World) { if (effective == Mount::World) {
overlay_check(overlay->SetOverlayTransformAbsolute(handle, vr::TrackingUniverseStanding, &world_transform), overlay, Matrix34 pose{};
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) pose[r][c] = world_transform.m[r][c];
pose = resized_mount_pose(pose, base_width, size_scale, float(H) / W);
vr::HmdMatrix34_t transform{};
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) transform.m[r][c] = pose[r][c];
overlay_check(overlay->SetOverlayTransformAbsolute(handle, vr::TrackingUniverseStanding, &transform), overlay,
"SetOverlayTransformAbsolute"); "SetOverlayTransformAbsolute");
} else { } else {
const auto pose = relative_mount_pose(effective, config.wrist); const auto pose = resized_mount_pose(relative_mount_pose(effective, config.wrist),
base_width, size_scale, float(H) / W);
vr::HmdMatrix34_t transform{}; vr::HmdMatrix34_t transform{};
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) transform.m[r][c] = pose[r][c]; for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) transform.m[r][c] = pose[r][c];
overlay_check(overlay->SetOverlayTransformTrackedDeviceRelative(handle, target, &transform), overlay, overlay_check(overlay->SetOverlayTransformTrackedDeviceRelative(handle, target, &transform), overlay,
"SetOverlayTransformTrackedDeviceRelative"); "SetOverlayTransformTrackedDeviceRelative");
} }
overlay_check(overlay->SetOverlayWidthInMeters(handle, mount_width(effective, config.wrist)), overlay, "SetOverlayWidthInMeters"); overlay_check(overlay->SetOverlayWidthInMeters(handle, base_width * size_scale), overlay, "SetOverlayWidthInMeters");
applied_mount = effective; applied_mount = effective;
anchor = target; anchor = target;
size_changed = false;
} }
placed = true; placed = true;
visibility(); visibility();
@@ -331,7 +342,11 @@ struct Overlay::Impl {
// a release must still hit the same enabled control. // a release must still hit the same enabled control.
last_pointer_event = "overlay focus changed"; break; last_pointer_event = "overlay focus changed"; break;
case vr::VREvent_MouseMove: case vr::VREvent_MouseMove:
surface.pointer_move(event.data.mouse.cursorIndex, event.data.mouse.x, H - event.data.mouse.y); if (auto factor = surface.pointer_move(event.data.mouse.cursorIndex, event.data.mouse.x,
H - event.data.mouse.y)) {
const float next = std::clamp(size_scale * *factor, .5f, 2.f);
if (next != size_scale) { size_scale = next; size_changed = true; }
}
break; break;
case vr::VREvent_MouseButtonDown: case vr::VREvent_MouseButtonDown:
++pointer_downs; ++pointer_downs;
+25 -3
View File
@@ -13,6 +13,7 @@ namespace frameyap {
namespace { namespace {
constexpr int W = PanelSurface::width, H = PanelSurface::height; constexpr int W = PanelSurface::width, H = PanelSurface::height;
using Color = Rgba; using Color = Rgba;
constexpr int grip_x = 927, grip_y = 646, grip_w = 51, grip_h = 29;
float rounded_distance(float px, float py, int x, int y, int w, int h, float radius) { float rounded_distance(float px, float py, int x, int y, int w, int h, float radius) {
const float dx = std::abs(px - (x + w / 2.f)) - (w / 2.f - radius); const float dx = std::abs(px - (x + w / 2.f)) - (w / 2.f - radius);
const float dy = std::abs(py - (y + h / 2.f)) - (h / 2.f - radius); const float dy = std::abs(py - (y + h / 2.f)) - (h / 2.f - radius);
@@ -109,6 +110,8 @@ struct PanelSurface::Impl {
bool dirty = true, lasers_anytime = false, advanced_debug = false; bool dirty = true, lasers_anytime = false, advanced_debug = false;
std::string placement_note, binding_note; std::string placement_note, binding_note;
std::array<int, 2> pressed{{-1, -1}}; std::array<int, 2> pressed{{-1, -1}};
int resize_cursor = -1;
float resize_x = 0.f, resize_y = 0.f;
std::vector<std::string> lines; std::vector<std::string> lines;
size_t page = 0; size_t page = 0;
static constexpr size_t lines_per_page = 4; static constexpr size_t lines_per_page = 4;
@@ -262,6 +265,7 @@ struct PanelSurface::Impl {
} }
void reset() { void reset() {
pressed.fill(-1); pressed.fill(-1);
resize_cursor = -1;
} }
bool render(const Panel& p) { bool render(const Panel& p) {
if (p.recording != panel.recording || p.enabled != panel.enabled || if (p.recording != panel.recording || p.enabled != panel.enabled ||
@@ -334,6 +338,11 @@ struct PanelSurface::Impl {
active ? cyan : muted, b.r.x + b.r.w - 12); active ? cyan : muted, b.r.x + b.r.w - 12);
} }
} }
// Separate from the Quit hitbox. The three diagonal marks remain
// visible on both tabs, slightly inset from the lower-right corner.
for (int n = 0; n < 3; ++n)
for (int i = 0; i < 13 + 5 * n; ++i)
rect({956 - i, 657 + 5 * n + i / 3, 2, 2}, cyan);
dirty = false; dirty = false;
return true; return true;
} }
@@ -344,17 +353,30 @@ PanelSurface::~PanelSurface() = default;
bool PanelSurface::render(const Panel& p) { return impl_->render(p); } bool PanelSurface::render(const Panel& p) { return impl_->render(p); }
const std::vector<unsigned char>& PanelSurface::pixels() const { return impl_->pixels; } const std::vector<unsigned char>& PanelSurface::pixels() const { return impl_->pixels; }
bool PanelSurface::available(UiAction a) const { return impl_->available(a); } bool PanelSurface::available(UiAction a) const { return impl_->available(a); }
void PanelSurface::pointer_move(unsigned, float, float) { std::optional<float> PanelSurface::pointer_move(unsigned cursor, float x, float y) {
// Hit-test on down/up only; laser movement does not change panel content if (cursor >= impl_->pressed.size() || int(cursor) != impl_->resize_cursor ||
// and does not require a GPU texture upload. !std::isfinite(x) || !std::isfinite(y)) return {};
// OpenVR supplies coordinates on the *current* panel. Work incrementally
// so resizing never depends on a stale down-position after a width change.
const float delta = (x - impl_->resize_x) / W - (y - impl_->resize_y) / H;
impl_->resize_x = x; impl_->resize_y = y;
return std::clamp(1.f + delta, .9f, 1.1f);
} }
void PanelSurface::pointer_down(unsigned cursor, float x, float y) { void PanelSurface::pointer_down(unsigned cursor, float x, float y) {
if (cursor >= impl_->pressed.size()) return; if (cursor >= impl_->pressed.size()) return;
if (impl_->resize_cursor < 0 && std::isfinite(x) && std::isfinite(y) &&
x >= grip_x && x < grip_x + grip_w && y >= grip_y && y < grip_y + grip_h) {
impl_->resize_cursor = int(cursor);
impl_->resize_x = x; impl_->resize_y = y;
impl_->pressed[cursor] = -1;
return;
}
impl_->pressed[cursor] = impl_->hit(x, y); impl_->pressed[cursor] = impl_->hit(x, y);
} }
SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y) { SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y) {
SurfaceEvent result; SurfaceEvent result;
if (cursor >= impl_->pressed.size()) return result; if (cursor >= impl_->pressed.size()) return result;
if (impl_->resize_cursor == int(cursor)) { impl_->resize_cursor = -1; return result; }
int index = std::exchange(impl_->pressed[cursor], -1); int index = std::exchange(impl_->pressed[cursor], -1);
if (index < 0 || impl_->hit(x, y) != index) return result; if (index < 0 || impl_->hit(x, y) != index) return result;
auto c = buttons[index].id; auto c = buttons[index].id;
+2 -1
View File
@@ -28,7 +28,8 @@ public:
const std::vector<unsigned char>& pixels() const; const std::vector<unsigned char>& pixels() const;
void pointer_down(unsigned cursor, float x, float y); void pointer_down(unsigned cursor, float x, float y);
SurfaceEvent pointer_up(unsigned cursor, float x, float y); SurfaceEvent pointer_up(unsigned cursor, float x, float y);
void pointer_move(unsigned cursor, float x, float y); // Returns a relative physical-width change while dragging the resize grip.
std::optional<float> pointer_move(unsigned cursor, float x, float y);
void reset_pointers(); void reset_pointers();
void set_placement_note(std::string note); void set_placement_note(std::string note);
void set_lasers_anytime(bool enabled); void set_lasers_anytime(bool enabled);
+13
View File
@@ -65,6 +65,19 @@ int main() {
const auto head = relative_mount_pose(Mount::Head); const auto head = relative_mount_pose(Mount::Head);
check_basis(head, 1, 0, 0, 1); 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)); CHECK(near(head[0][3], 0)); CHECK(near(head[1][3], -.16f)); CHECK(near(head[2][3], -1.05f));
for (auto mount : mounts) {
const float width = mount_width(mount);
const auto original = mount == Mount::World ? identity : relative_mount_pose(mount);
const auto grown = resized_mount_pose(original, width, 1.5f, .68f);
const auto shrunk = resized_mount_pose(original, width, .5f, .68f);
CHECK(resized_mount_pose(original, width, 1.f, .68f) == original);
for (int row = 0; row < 3; ++row) {
// Upper-left = center - width/2 * right + height/2 * up.
const float old_corner = original[row][3] - width * .5f * original[row][0] + width * .34f * original[row][1];
CHECK(near(grown[row][3] - width * .75f * grown[row][0] + width * .51f * grown[row][1], old_corner));
CHECK(near(shrunk[row][3] - width * .25f * shrunk[row][0] + width * .17f * shrunk[row][1], old_corner));
}
}
const auto left = relative_mount_pose(Mount::LeftWrist), right = relative_mount_pose(Mount::RightWrist); const auto left = relative_mount_pose(Mount::LeftWrist), right = relative_mount_pose(Mount::RightWrist);
check_basis(right, 0, 1, -1, 0); // right wrist faces inward, not away from wearer check_basis(right, 0, 1, -1, 0); // right wrist faces inward, not away from wearer
for (int r = 0; r < 3; ++r) { for (int r = 0; r < 3; ++r) {
+13
View File
@@ -61,6 +61,19 @@ int main(int argc, char** argv) {
no_action(click(surface, std::numeric_limits<float>::quiet_NaN(), 610)); no_action(click(surface, std::numeric_limits<float>::quiet_NaN(), 610));
no_action(click(surface, -1, 610)); no_action(click(surface, -1, 610));
no_action(click(surface, 1000, 680)); no_action(click(surface, 1000, 680));
// Lower-right grip scales without activating the nearby Quit button or
// uploading new pixels. Other cursors cannot hijack an active drag.
surface.pointer_down(0, 951, 656);
assert(!surface.pointer_move(1, 970, 650));
auto factor = surface.pointer_move(0, 971, 641);
assert(factor && *factor > 1.f);
no_action(surface.pointer_up(0, 900, 610));
assert(!surface.pointer_move(0, 975, 640));
assert(!surface.render(p));
surface.pointer_down(0, 951, 656);
surface.reset_pointers();
assert(!surface.pointer_move(0, 971, 641));
no_action(surface.pointer_up(0, 951, 656));
p.enabled = true; p.enabled = true;
p.record_available = false; p.record_available = false;