From a9d6762ebd3683113c2cebd33389d822f1581e15 Mon Sep 17 00:00:00 2001 From: baketnk Date: Thu, 24 Sep 2026 13:59:45 -0400 Subject: [PATCH] Add opt-in visible-overlay laser mode setting --- CMakeLists.txt | 6 ++- README.md | 9 +++-- docs/overlay.md | 31 +++++++++++---- docs/poc.md | 2 + src/laser_setting.cpp | 73 ++++++++++++++++++++++++++++++++++++ src/laser_setting.hpp | 9 +++++ src/main.cpp | 3 +- src/overlay.cpp | 39 ++++++++++++++++--- src/panel_surface.cpp | 33 +++++++++++----- src/panel_surface.hpp | 2 + tests/laser_setting_test.cpp | 35 +++++++++++++++++ tests/panel_test.cpp | 11 +++++- 12 files changed, 225 insertions(+), 28 deletions(-) create mode 100644 src/laser_setting.cpp create mode 100644 src/laser_setting.hpp create mode 100644 tests/laser_setting_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ce027b..63ab9b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,7 +46,7 @@ add_executable(frameyap src/main.cpp) target_link_libraries(frameyap PRIVATE frameyap_core) target_compile_definitions(frameyap PRIVATE FRAMEYAP_VERSION="${FRAMEYAP_VERSION}") if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - add_library(frameyap_mount src/mount.cpp) + add_library(frameyap_mount src/mount.cpp src/laser_setting.cpp) target_include_directories(frameyap_mount PUBLIC src) add_library(frameyap_worker src/worker.cpp) target_include_directories(frameyap_worker PUBLIC src) @@ -116,6 +116,10 @@ if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING) add_executable(frameyap_mount_test tests/mount_test.cpp) target_link_libraries(frameyap_mount_test PRIVATE frameyap_mount) add_test(NAME frameyap.mount COMMAND frameyap_mount_test) + add_executable(frameyap_laser_setting_test tests/laser_setting_test.cpp) + target_link_libraries(frameyap_laser_setting_test PRIVATE frameyap_mount) + target_compile_options(frameyap_laser_setting_test PRIVATE -UNDEBUG) + add_test(NAME frameyap.laser_setting COMMAND frameyap_laser_setting_test) add_executable(frameyap_lock_test tests/lock_test.cpp) target_include_directories(frameyap_lock_test PRIVATE src) add_test(NAME frameyap.instance_lock COMMAND frameyap_lock_test) diff --git a/README.md b/README.md index ba9ed1b..dfb1e8c 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,16 @@ See [third-party notes](docs/third-party.md). No GitHub release is published yet ## Controls - **Right X (default Frame binding):** hold to record; release to - transcribe. This PTT action is remappable through SteamVR bindings; delivery - in the dashboard still requires an on-device test. + transcribe. Repeated presses reached the controls-only diagnostic on Frame; + live mic capture through this shortcut still needs guided acceptance. This + PTT action is remappable through SteamVR bindings. - **Left grip (when active):** double-tap to explicitly send Enter. Never inferred from speech. - **Overlay:** Record/Stop, Cancel, paginated preview, Insert, Enter and Quit. Review and settings share one Inconsolata/neon-framed surface. World-space by default; settings offer left wrist, right wrist and head mounting, plus recenter. - Dashboard lasers provide clickable controls without forcing global laser mode. + Dashboard lasers provide clickable controls. Settings → Lasers anytime is an + opt-in, default-off system-wide laser mode while the panel is visible; it may + affect games and is separate from experimental input overrides. - **Theme and controls:** optional `$XDG_CONFIG_HOME/frameyap/config.json` selects panel colors, a font path and Frame controller button mappings; missing fonts fall back to bundled Inconsolata. The installer creates/checks this file and diff --git a/docs/overlay.md b/docs/overlay.md index 1d40e6d..36dca3a 100644 --- a/docs/overlay.md +++ b/docs/overlay.md @@ -107,10 +107,25 @@ keeps the selection for the session and displays a warning. `--mount world|left-wrist|right-wrist|head` overrides the saved choice for one launch without writing it; `--head` remains an alias for `--mount head`. +Settings also has **Lasers anytime** (default off). When enabled, FrameYap sets +OpenVR's `VROverlayFlags_MakeOverlaysInteractiveIfVisible` on its panel. OpenVR +requests system-wide laser mouse mode while the panel is visible, including +with Steam's dashboard closed; it may change interaction with games. Turning +it off removes that request. This is **not** the experimental overlay action +priority override and does not promise pass-through of a dashboard-owned +button or PTT delivery during dashboard focus. The choice is saved as `on` or +`off` in `$XDG_CONFIG_HOME/frameyap/lasers-anytime` (default +`~/.config/frameyap/lasers-anytime`) using a private atomic file replacement; +missing, symlinked or invalid files mean off. A failed save keeps the new +choice only for the running session and shows a warning. To turn it back on +after disabling it with the dashboard closed, open the dashboard to use its +laser on the Settings button. Controls-only checks can toggle it temporarily +but do not save the preference. This has not yet been accepted in a headset. + ### Hardware-free UI checks -The default build tests mount parsing, persistence and pose geometry without any -native dependencies. A FreeType-only opt-in build exercises the actual renderer, +The default build tests mount parsing, laser preference persistence and pose +geometry without any native dependencies. A FreeType-only opt-in build exercises the actual renderer, pointer gating, tab switches, pagination, recording state and redraw invalidation: ```sh @@ -136,11 +151,13 @@ updates are flicker-free. `assets/actions.json` names six actions: left/right grip, PTT, cancel, insert, Enter. `bindings_frame_controller.json` maps right X click to hold-to-talk PTT; -the grip bindings remain for optional remapping/diagnosis. Both grip actions -were **inactive** during the dashboard controls check despite a loaded binding -file; this does not prove whether right X will deliver until an on-device test. -`bindings_knuckles.json` is an -additional **Index/knuckles example only**. Collisions with scene actions require +the grip bindings remain for optional remapping/diagnosis. In one dashboard +probe grips were inactive; a later controls-only probe delivered repeated right +X PTT BeginRecord/EndRecord callbacks. The wearer reports controller actions +are usable with Steam's dashboard closed, not with the dashboard itself open. +Neither probe used a microphone or established game-scene pass-through. +`bindings_knuckles.json` is an additional **Index/knuckles example only**. +Collisions with scene actions require separate on-device validation. Left grip double tap (releases <=250 ms, second press within 350 ms) requests explicit Enter only when enabled. Right grip: first short squeeze and release (<=250 ms), then diff --git a/docs/poc.md b/docs/poc.md index ebc9e7b..300d7a5 100644 --- a/docs/poc.md +++ b/docs/poc.md @@ -7,6 +7,8 @@ initialize OpenVR, open a microphone, run ASR, download files or inject input. - Opt-in OpenVR RGBA overlay with world-space default and selectable wrist/head mounts, status, recording timer, paginated UTF-8 preview and explicit controls. + A saved, default-off Lasers anytime setting requests system-wide laser mouse + mode while FrameYap is visible; it may affect games, and is not an input override. - Remappable SteamVR actions. The default Steam Frame binding maps right X (hold to record, release to transcribe) to the existing PTT action using the observed `frame_controller` profile. Grip bindings remain, but both grip diff --git a/src/laser_setting.cpp b/src/laser_setting.cpp new file mode 100644 index 0000000..831d5b9 --- /dev/null +++ b/src/laser_setting.cpp @@ -0,0 +1,73 @@ +#include "laser_setting.hpp" +#include "mount.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +namespace frameyap { +namespace { +std::atomic next_temp{0}; +} + +std::filesystem::path default_laser_settings_path() { + auto mount_path = default_mount_settings_path(); + return mount_path.empty() ? mount_path : mount_path.parent_path() / "lasers-anytime"; +} + +bool load_lasers_anytime(const std::filesystem::path& path) { + try { + if (path.empty() || std::filesystem::is_symlink(path) || !std::filesystem::is_regular_file(path)) return false; + std::ifstream input(path, std::ios::binary); + if (!input) return false; + char data[9]{}; + input.read(data, sizeof(data)); + const auto size = input.gcount(); + return size == 3 && std::string_view(data, 3) == "on\n"; + } catch (...) { return false; } +} + +bool save_lasers_anytime(const std::filesystem::path& path, bool enabled) { + std::filesystem::path temporary; + int fd = -1; + try { + if (path.empty() || path.filename().empty() || std::filesystem::is_symlink(path)) return false; + const auto dir = path.has_parent_path() ? path.parent_path() : std::filesystem::path("."); + std::error_code ec; + std::filesystem::create_directories(dir, ec); + if (ec) return false; + for (int attempt = 0; attempt < 16; ++attempt) { + temporary = dir / (path.filename().string() + ".tmp." + std::to_string(::getpid()) + + "." + std::to_string(next_temp.fetch_add(1))); + fd = ::open(temporary.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600); + if (fd >= 0) break; + if (errno != EEXIST) return false; + } + if (fd < 0) return false; + const std::string_view content = enabled ? "on\n" : "off\n"; + size_t written = 0; + while (written < content.size()) { + const auto n = ::write(fd, content.data() + written, content.size() - written); + if (n < 0 && errno == EINTR) continue; + if (n <= 0) { ::close(fd); fd = -1; std::filesystem::remove(temporary, ec); return false; } + written += static_cast(n); + } + const bool synced = ::fsync(fd) == 0; + const bool closed = ::close(fd) == 0; + fd = -1; + if (!synced || !closed) { std::filesystem::remove(temporary, ec); return false; } + std::filesystem::rename(temporary, path, ec); + if (ec) { std::filesystem::remove(temporary, ec); return false; } + return true; + } catch (...) { + if (fd >= 0) ::close(fd); + if (!temporary.empty()) { std::error_code ec; std::filesystem::remove(temporary, ec); } + return false; + } +} +} // namespace frameyap diff --git a/src/laser_setting.hpp b/src/laser_setting.hpp new file mode 100644 index 0000000..94029eb --- /dev/null +++ b/src/laser_setting.hpp @@ -0,0 +1,9 @@ +#pragma once +#include + +namespace frameyap { +// Opt-in, user-local preference. Missing or malformed files always mean off. +std::filesystem::path default_laser_settings_path(); +bool load_lasers_anytime(const std::filesystem::path& path); +bool save_lasers_anytime(const std::filesystem::path& path, bool enabled); +} // namespace frameyap diff --git a/src/main.cpp b/src/main.cpp index 9354f3f..fece564 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,7 +22,8 @@ void help() { " --check-overlay --assets DIR [--font FILE] [--mount MODE] (5s, no mic/input)\n" " --check-controls --assets DIR [--font FILE] [--mount MODE] (30s, gestures only)\n\n" "Mount: world (first-launch default), left-wrist, right-wrist, head.\n" - "Settings save the mount; --mount overrides it for this launch. --head is an alias.\n" + "Settings save the mount and opt-in Lasers anytime mode (may affect games).\n" + "--mount overrides placement for this launch. --head is an alias.\n" "Theme, font and Frame button mappings: $XDG_CONFIG_HOME/frameyap/config.json\n" "(or ~/.config/frameyap/config.json). CLI --font overrides config; missing fonts\n" "fall back to bundled Inconsolata, then a system DejaVu face.\n" diff --git a/src/overlay.cpp b/src/overlay.cpp index eb7e739..6b68ba5 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -1,5 +1,6 @@ #include "overlay.hpp" #include "gestures.hpp" +#include "laser_setting.hpp" #include "panel_surface.hpp" #include @@ -50,7 +51,9 @@ struct Overlay::Impl { vr::VRActionSetHandle_t action_set = vr::k_ulInvalidActionSetHandle; std::array actions{}; std::filesystem::path settings_path; + std::filesystem::path laser_settings_path; Mount mount; + bool lasers_anytime = false, laser_change_failed = false; Config config; PanelSurface surface; Panel panel; @@ -75,7 +78,9 @@ struct Overlay::Impl { Impl(const std::string& assets, const std::string& font, std::optional requested, bool persist) : settings_path(default_mount_settings_path()), + laser_settings_path(default_laser_settings_path()), mount(requested ? *requested : load_mount(settings_path)), + lasers_anytime(load_lasers_anytime(laser_settings_path)), config(load_config(default_config_path())), surface(resolve_font(assets, font.empty() ? config.font : font), mount, config.theme), persist_mount(persist) { @@ -107,8 +112,14 @@ struct Overlay::Impl { overlay_check(overlay->CreateOverlay("local.frameyap.overlay.panel", "FrameYap", &handle), overlay, "CreateOverlay"); overlay_check(overlay->SetOverlayWidthInMeters(handle, 0.85f), overlay, "SetOverlayWidthInMeters"); overlay_check(overlay->SetOverlayInputMethod(handle, vr::VROverlayInputMethod_Mouse), overlay, "SetOverlayInputMethod"); - // Let dashboard lasers reach our controls without forcing global - // laser-mouse mode over a running scene. + // Normal priority actions stay unchanged. This separate, explicit + // preference requests system-wide laser mouse mode only while the + // panel is visible; it may affect interaction with a running game. + if (lasers_anytime && overlay->SetOverlayFlag(handle, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true) != vr::VROverlayError_None) { + lasers_anytime = false; + laser_change_failed = true; + } + surface.set_lasers_anytime(lasers_anytime); overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_VisibleInDashboard, true), overlay, "VisibleInDashboard"); vr::HmdVector2_t mouse_scale{{float(W), float(H)}}; overlay_check(overlay->SetOverlayMouseScale(handle, &mouse_scale), overlay, "SetOverlayMouseScale"); @@ -144,7 +155,8 @@ struct Overlay::Impl { } if (effective == Mount::World && applied_mount && *applied_mount != Mount::World) world_ready = false; // a fresh world fallback near the wearer, not an old room location - std::string note = save_failed ? "Preference could not be saved; using it for this session." : ""; + std::string note = laser_change_failed ? "SteamVR declined the laser mode change." : + save_failed ? "Preference could not be saved; using it for this session." : ""; if (effective != mount) note = save_failed ? "Wrist untracked; world fallback. Preference not saved." : "Wrist not tracked - using world space until it returns."; if (effective == Mount::World && !world_ready) { @@ -273,8 +285,20 @@ struct Overlay::Impl { last_pointer_event = "up button=" + std::to_string(event.data.mouse.button); if (event.data.mouse.button == vr::VRMouseButton_Left) { auto event_result = surface.pointer_up(event.data.mouse.cursorIndex, event.data.mouse.x, H - event.data.mouse.y); - if (event_result.action || event_result.mount || event_result.recenter) ++pointer_actions; + if (event_result.action || event_result.mount || event_result.recenter || event_result.lasers_anytime) ++pointer_actions; if (event_result.action) result.push_back(*event_result.action); + if (event_result.lasers_anytime) { + const bool enabled = *event_result.lasers_anytime; + if (overlay->SetOverlayFlag(handle, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, + enabled) == vr::VROverlayError_None) { + lasers_anytime = enabled; + laser_change_failed = false; + surface.set_lasers_anytime(enabled); + save_failed = persist_mount && !save_lasers_anytime(laser_settings_path, enabled); + } else { + laser_change_failed = true; + } + } if (event_result.mount) { mount = *event_result.mount; save_failed = persist_mount && !save_mount(settings_path, mount); @@ -335,7 +359,12 @@ void Overlay::draw(const Panel& panel) { impl_->draw(panel); } std::string Overlay::controls_status() { // Diagnostic only: distinguish SteamVR binding/activity from our stricter // pose/role gate. Raw legacy state is read-only and may be unavailable. - std::string result = "SteamVR update=" + std::to_string(int(impl_->action_update_error)); + bool laser_flag = false; + const auto laser_error = impl_->overlay->GetOverlayFlag(impl_->handle, + vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, &laser_flag); + std::string result = "SteamVR update=" + std::to_string(int(impl_->action_update_error)) + + " dashboard=" + (impl_->overlay->IsDashboardVisible() ? "Y" : "N") + + " lasers-anytime=" + (laser_error == vr::VROverlayError_None ? (laser_flag ? "Y" : "N") : "n/a"); for (size_t i = 0; i < 2; ++i) { const auto role = i == 0 ? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand; const auto device = impl_->system->GetTrackedDeviceIndexForControllerRole(role); diff --git a/src/panel_surface.cpp b/src/panel_surface.cpp index e11f18f..69dd01d 100644 --- a/src/panel_surface.cpp +++ b/src/panel_surface.cpp @@ -26,9 +26,9 @@ struct Rect { } }; enum class Control { Review, Settings, Prev, Next, Record, Cancel, Insert, Enter, Quit, - World, Left, Right, Head, Recenter }; + World, Left, Right, Head, Recenter, LasersAnytime }; struct Button { Rect r; Control id; const char* label; }; -constexpr std::array buttons{{ +constexpr std::array buttons{{ {{32, 138, 180, 46}, Control::Review, "Review"}, {{226, 138, 180, 46}, Control::Settings, "Settings"}, {{32, 406, 154, 44}, Control::Prev, "Previous"}, @@ -43,6 +43,7 @@ constexpr std::array buttons{{ {{32, 308, 454, 58}, Control::Left, "Left wrist"}, {{514, 308, 454, 58}, Control::Right, "Right wrist"}, {{32, 394, 300, 50}, Control::Recenter, "Recenter in front"}, + {{514, 394, 454, 50}, Control::LasersAnytime, "Lasers anytime"}, }}; std::optional action(Control c) { switch (c) { @@ -100,7 +101,7 @@ struct PanelSurface::Impl { Mount mount; Theme theme; Color background, card, ink, muted, cyan, pink; - bool settings = false, dirty = true; + bool settings = false, dirty = true, lasers_anytime = false; std::string placement_note; std::array pressed{{-1, -1}}; std::vector lines; @@ -238,7 +239,7 @@ struct PanelSurface::Impl { } bool visible(Control c) const { if (c == Control::Prev || c == Control::Next) return !settings; - if (mounting(c) || c == Control::Recenter) return settings; + if (mounting(c) || c == Control::Recenter || c == Control::LasersAnytime) return settings; return true; } bool enabled(Control c) const { @@ -294,11 +295,11 @@ struct PanelSurface::Impl { text(detail[i], 32, 486 + int(i) * 30, 24, muted, 936); if (detail.size() > 2) text("[detail truncated]", 730, 546, 22, pink, 968); } else { - text("MOUNT THE MENU", 32, 224, 22, muted, 968); - text("World stays put. Recenter places it in front of you.", 354, 426, 22, muted, 968); - text(placement_note.empty() ? "Choose a mount. Preference is saved on this device." : placement_note, - 32, 486, 23, muted, 968); - text("Tracking lost? Wrist placement falls back to world space.", 32, 518, 23, muted, 968); + text("MOUNT AND INTERACTION", 32, 224, 22, muted, 968); + text("Lasers anytime enables system-wide laser mode while this panel is visible.", 32, 472, 20, muted, 968); + text(placement_note.empty() ? "May affect games. Default off; changes saved on this device." : placement_note, + 32, 509, 22, muted, 968); + text("If off, open the dashboard to click this setting again.", 32, 538, 20, muted, 968); } rect({32, 550, 936, 1}, mix(card, cyan, .17f)); for (size_t i = 0; i < buttons.size(); ++i) { @@ -306,7 +307,8 @@ struct PanelSurface::Impl { if (!visible(b.id)) continue; bool on = enabled(b.id); bool selected = (b.id == Control::Review && !settings) || (b.id == Control::Settings && settings) || - (mounting(b.id) && *mounting(b.id) == mount); + (mounting(b.id) && *mounting(b.id) == mount) || + (b.id == Control::LasersAnytime && lasers_anytime); const Color fill = !on ? mix(background, card, .40f) : selected ? mix(card, cyan, .14f) : card; const Color accent = b.id == Control::Record && panel.recording ? pink : cyan; @@ -318,6 +320,9 @@ struct PanelSurface::Impl { const auto label = b.id == Control::Record && panel.recording ? "Stop" : b.label; text(label, b.r.x + 16, b.r.y + b.r.h / 2 + 9, 27, on ? ink : mix(background, muted, .48f), b.r.x + b.r.w - 8); if (mounting(b.id) && selected) text("ON", b.r.x + b.r.w - 56, b.r.y + 38, 23, cyan, b.r.x + b.r.w - 12); + if (b.id == Control::LasersAnytime) + text(lasers_anytime ? "ON" : "OFF", b.r.x + b.r.w - 66, b.r.y + 34, 22, + lasers_anytime ? cyan : muted, b.r.x + b.r.w - 12); } dirty = false; return true; @@ -347,6 +352,7 @@ SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y) { else if (auto a = action(c)) result.action = a; else if (auto m = mounting(c)) { impl_->mount = *m; result.mount = *m; impl_->reset(); impl_->dirty = true; } else if (c == Control::Recenter) { result.recenter = true; impl_->reset(); } + else if (c == Control::LasersAnytime) result.lasers_anytime = !impl_->lasers_anytime; else if (c == Control::Review || c == Control::Settings) { impl_->settings = c == Control::Settings; impl_->reset(); impl_->dirty = true; } else if (c == Control::Prev) { --impl_->page; impl_->dirty = true; } else if (c == Control::Next) { ++impl_->page; impl_->dirty = true; } @@ -356,4 +362,11 @@ void PanelSurface::reset_pointers() { impl_->reset(); } void PanelSurface::set_placement_note(std::string note) { if (impl_->placement_note != note) { impl_->placement_note = std::move(note); impl_->dirty = true; } } +void PanelSurface::set_lasers_anytime(bool enabled) { + if (impl_->lasers_anytime != enabled) { + impl_->lasers_anytime = enabled; + impl_->reset(); + impl_->dirty = true; + } +} } // namespace frameyap diff --git a/src/panel_surface.hpp b/src/panel_surface.hpp index d3328b7..8f8c8f0 100644 --- a/src/panel_surface.hpp +++ b/src/panel_surface.hpp @@ -11,6 +11,7 @@ namespace frameyap { struct SurfaceEvent { std::optional action; std::optional mount; + std::optional lasers_anytime; bool recenter = false; }; // One CPU RGBA canvas, independent of OpenVR. Settings replace the review area; @@ -29,6 +30,7 @@ public: void pointer_move(unsigned cursor, float x, float y); void reset_pointers(); void set_placement_note(std::string note); + void set_lasers_anytime(bool enabled); bool available(UiAction action) const; private: struct Impl; diff --git a/tests/laser_setting_test.cpp b/tests/laser_setting_test.cpp new file mode 100644 index 0000000..58a636b --- /dev/null +++ b/tests/laser_setting_test.cpp @@ -0,0 +1,35 @@ +#include "laser_setting.hpp" +#include +#include +#include +#include +#include +#include + +int main() { + using namespace frameyap; + const auto pattern = (std::filesystem::temp_directory_path() / "frameyap-laser-test-XXXXXX").string(); + std::vector buffer(pattern.begin(), pattern.end()); + buffer.push_back('\0'); + const char* created = ::mkdtemp(buffer.data()); + assert(created); + const std::filesystem::path root(created); + assert(::setenv("XDG_CONFIG_HOME", root.c_str(), 1) == 0); + assert(default_laser_settings_path() == root / "frameyap/lasers-anytime"); + const auto path = default_laser_settings_path(); + assert(!load_lasers_anytime(path)); + assert(save_lasers_anytime(path, true)); + assert(load_lasers_anytime(path)); + assert((std::filesystem::status(path).permissions() & + (std::filesystem::perms::group_all | std::filesystem::perms::others_all)) == std::filesystem::perms::none); + assert(save_lasers_anytime(path, false)); + assert(!load_lasers_anytime(path)); + { std::ofstream out(path, std::ios::trunc); out << "anything else\n"; } + assert(!load_lasers_anytime(path)); + const auto link = root / "link"; + std::filesystem::create_symlink(path, link); + assert(!load_lasers_anytime(link)); + assert(!save_lasers_anytime(link, true)); + assert(!load_lasers_anytime(path)); + std::filesystem::remove_all(root); +} diff --git a/tests/panel_test.cpp b/tests/panel_test.cpp index 863e18b..aaa8db1 100644 --- a/tests/panel_test.cpp +++ b/tests/panel_test.cpp @@ -11,7 +11,9 @@ SurfaceEvent click(PanelSurface& surface, float x, float y, unsigned cursor = 0) surface.pointer_down(cursor, x, y); return surface.pointer_up(cursor, x, y); } -void no_action(const SurfaceEvent& event) { assert(!event.action && !event.mount && !event.recenter); } +void no_action(const SurfaceEvent& event) { + assert(!event.action && !event.mount && !event.lasers_anytime && !event.recenter); +} void snapshot(PanelSurface& surface, const std::string& path) { std::ofstream out(path, std::ios::binary); assert(out); @@ -104,6 +106,12 @@ int main(int argc, char** argv) { assert(click(surface, 680, 332).mount == Mount::RightWrist); assert(click(surface, 680, 260).mount == Mount::Head); assert(click(surface, 180, 260).mount == Mount::World); + auto laser = click(surface, 680, 420); + assert(laser.lasers_anytime == true && !laser.action && !laser.mount); + surface.set_lasers_anytime(true); assert(surface.render(p)); + laser = click(surface, 680, 420); + assert(laser.lasers_anytime == false && !laser.action && !laser.mount); + surface.set_lasers_anytime(false); assert(surface.render(p)); assert(click(surface, 280, 610).action == UiAction::Cancel); surface.set_placement_note("Wrist not tracked - using world space until it returns."); assert(surface.render(p)); assert(!surface.render(p)); @@ -114,6 +122,7 @@ int main(int argc, char** argv) { no_action(click(surface, 100, 160)); no_action(surface.pointer_up(1, 680, 260)); no_action(click(surface, 680, 260)); // mount controls not active on Review + no_action(click(surface, 680, 420)); // laser toggle only exists on Settings // Long UTF-8, newlines and malformed bytes are bounded, paginated and navigable. p.transcript.clear();