From 19a3db76676a40cb1ec83a394c6e30a02897b64d Mon Sep 17 00:00:00 2001 From: baketnk Date: Thu, 24 Sep 2026 15:50:55 -0400 Subject: [PATCH] Add session-only anchored overlay resize grip --- README.md | 2 ++ docs/overlay.md | 8 +++++++- src/mount.cpp | 8 ++++++++ src/mount.hpp | 3 +++ src/overlay.cpp | 27 +++++++++++++++++++++------ src/panel_surface.cpp | 28 +++++++++++++++++++++++++--- src/panel_surface.hpp | 3 ++- tests/mount_test.cpp | 13 +++++++++++++ tests/panel_test.cpp | 13 +++++++++++++ 9 files changed, 94 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f150ce7..dbd937a 100644 --- a/README.md +++ b/README.md @@ -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. - **Overlay:** Record/Stop, Cancel, paginated preview, Insert, Enter and Quit. 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 default; settings offer left wrist, right wrist and head mounting, plus recenter. Dashboard lasers provide clickable controls. Settings → Lasers anytime is an diff --git a/docs/overlay.md b/docs/overlay.md index 93f6101..c684f7b 100644 --- a/docs/overlay.md +++ b/docs/overlay.md @@ -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. 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 (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 diff --git a/src/mount.cpp b/src/mount.cpp index b9bbcc5..da46f14 100644 --- a/src/mount.cpp +++ b/src/mount.cpp @@ -95,6 +95,14 @@ float mount_width(Mount mount, const WristPlacement& wrist) { 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() { try { const char* xdg = std::getenv("XDG_CONFIG_HOME"); diff --git a/src/mount.hpp b/src/mount.hpp index 02eed9e..e243cac 100644 --- a/src/mount.hpp +++ b/src/mount.hpp @@ -29,6 +29,9 @@ std::optional world_mount_pose(const Matrix34& hmd); // Device-relative transform; World has no relative anchor and returns identity. Matrix34 relative_mount_pose(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. std::filesystem::path default_mount_settings_path(); diff --git a/src/overlay.cpp b/src/overlay.cpp index cb34fc3..fe6290a 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -76,6 +76,8 @@ struct Overlay::Impl { Panel panel; bool save_failed = false, debug_save_failed = 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{}; std::optional applied_mount; vr::TrackedDeviceIndex_t anchor = vr::k_unTrackedDeviceIndexInvalid; @@ -211,21 +213,30 @@ struct Overlay::Impl { applied_mount.reset(); } surface.set_placement_note(note); - if (applied_mount != effective || (effective != Mount::World && anchor != target)) { - surface.reset_pointers(); // a release on a relocated surface cannot activate an old press + 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 + const float base_width = mount_width(effective, config.wrist); 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"); } 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{}; 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, "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; anchor = target; + size_changed = false; } placed = true; visibility(); @@ -331,7 +342,11 @@ struct Overlay::Impl { // a release must still hit the same enabled control. last_pointer_event = "overlay focus changed"; break; 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; case vr::VREvent_MouseButtonDown: ++pointer_downs; diff --git a/src/panel_surface.cpp b/src/panel_surface.cpp index 41bd258..faf23d6 100644 --- a/src/panel_surface.cpp +++ b/src/panel_surface.cpp @@ -13,6 +13,7 @@ namespace frameyap { namespace { constexpr int W = PanelSurface::width, H = PanelSurface::height; 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) { 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); @@ -109,6 +110,8 @@ struct PanelSurface::Impl { bool dirty = true, lasers_anytime = false, advanced_debug = false; std::string placement_note, binding_note; std::array pressed{{-1, -1}}; + int resize_cursor = -1; + float resize_x = 0.f, resize_y = 0.f; std::vector lines; size_t page = 0; static constexpr size_t lines_per_page = 4; @@ -262,6 +265,7 @@ struct PanelSurface::Impl { } void reset() { pressed.fill(-1); + resize_cursor = -1; } bool render(const Panel& p) { 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); } } + // 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; return true; } @@ -344,17 +353,30 @@ PanelSurface::~PanelSurface() = default; bool PanelSurface::render(const Panel& p) { return impl_->render(p); } const std::vector& PanelSurface::pixels() const { return impl_->pixels; } bool PanelSurface::available(UiAction a) const { return impl_->available(a); } -void PanelSurface::pointer_move(unsigned, float, float) { - // Hit-test on down/up only; laser movement does not change panel content - // and does not require a GPU texture upload. +std::optional PanelSurface::pointer_move(unsigned cursor, float x, float y) { + if (cursor >= impl_->pressed.size() || int(cursor) != impl_->resize_cursor || + !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) { 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); } SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y) { SurfaceEvent 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); if (index < 0 || impl_->hit(x, y) != index) return result; auto c = buttons[index].id; diff --git a/src/panel_surface.hpp b/src/panel_surface.hpp index 9e5a6ba..8ff6ecb 100644 --- a/src/panel_surface.hpp +++ b/src/panel_surface.hpp @@ -28,7 +28,8 @@ public: const std::vector& pixels() const; void pointer_down(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 pointer_move(unsigned cursor, float x, float y); void reset_pointers(); void set_placement_note(std::string note); void set_lasers_anytime(bool enabled); diff --git a/tests/mount_test.cpp b/tests/mount_test.cpp index 3366249..c9bdeeb 100644 --- a/tests/mount_test.cpp +++ b/tests/mount_test.cpp @@ -65,6 +65,19 @@ int main() { const auto head = relative_mount_pose(Mount::Head); 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)); + 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); check_basis(right, 0, 1, -1, 0); // right wrist faces inward, not away from wearer for (int r = 0; r < 3; ++r) { diff --git a/tests/panel_test.cpp b/tests/panel_test.cpp index caeb066..405cee7 100644 --- a/tests/panel_test.cpp +++ b/tests/panel_test.cpp @@ -61,6 +61,19 @@ int main(int argc, char** argv) { no_action(click(surface, std::numeric_limits::quiet_NaN(), 610)); no_action(click(surface, -1, 610)); 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.record_available = false;