mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 04:04:07 +02:00
Make wrist fallback optional and scroll Settings with laser
This commit is contained in:
1 parent
0f36a45358
commit
8f4c1877f7
15 files changed
+218
-30
No files matched your search
@@ -6,6 +6,7 @@
|
||||
"close_mic_when_idle": false,
|
||||
"backend": "redux",
|
||||
"lock_layout": false,
|
||||
"wrist_world_fallback": true,
|
||||
"clock_24h": false,
|
||||
"date_format": "mdy",
|
||||
"quick_inputs": ["/new", "/questions", "/help"],
|
||||
|
||||
+15
-3
@@ -74,7 +74,12 @@ release) and Lasers anytime (system-wide lasers may affect games). The header
|
||||
updates when the displayed minute, date, battery or dashboard state changes,
|
||||
not every frame. Settings toggles 12/24-hour time and cycles date Off →
|
||||
MM/DD/YYYY → DD/MM/YYYY → YYYY-MM-DD → Off. These only affect display;
|
||||
mount choices remain in Settings.
|
||||
mount choices remain in Settings. The Settings controls form one vertically
|
||||
scrollable list: hover the laser over the list and use the right stick to scroll.
|
||||
The tabs, status and action footer remain fixed. Scrolling cancels a pending
|
||||
pointer press rather than activating a different control. There are no Settings
|
||||
pages. OpenVR's discrete/smooth laser scroll events are requested; right-stick
|
||||
behavior with SteamVR's keyboard open still needs a live Frame check.
|
||||
|
||||
Settings → **About** replaces the settings area with static information: the
|
||||
build version (plus git commit when built from an untagged or modified tree),
|
||||
@@ -142,6 +147,7 @@ installer creates one with defaults on first install. Copy the shipped
|
||||
"close_mic_when_idle": false,
|
||||
"backend": "redux",
|
||||
"lock_layout": false,
|
||||
"wrist_world_fallback": true,
|
||||
"clock_24h": false,
|
||||
"date_format": "mdy",
|
||||
"quick_inputs": ["/new", "/questions", "/help"],
|
||||
@@ -390,7 +396,7 @@ fade from 60° to 75°, then hidden (including laser interaction). Pitch, yaw an
|
||||
roll contribute together; turning the wrist away or moving the head around it
|
||||
changes the angle. OpenVR's overlay alpha changes without rerendering the panel.
|
||||
World and head mounts do not fade. Missing headset tracking hides a wrist panel;
|
||||
a lost wrist still uses the existing world-space fallback. Headset readability,
|
||||
by default a lost wrist uses a world-space fallback. Headset readability,
|
||||
fade feel and interaction at the threshold still need live acceptance.
|
||||
|
||||
To tune the selected wrist, set `wrist` in `config.json` as in the example above:
|
||||
@@ -401,7 +407,13 @@ are read at startup, and do not alter world/head placement. Invalid values
|
||||
fail direct native startup; the installer backs up and repairs invalid entries.
|
||||
A missing/untracked selected wrist temporarily falls back to world space,
|
||||
with a visible explanation in Settings, then reattaches when tracking returns.
|
||||
The saved preference is not replaced by the fallback. These offsets and sizes are
|
||||
Use Settings → **Wrist world fallback** to switch between the default
|
||||
front-of-you fallback (ON) and hiding the panel when wrist tracking is lost
|
||||
(OFF). It reappears on the selected wrist when tracking returns. The toggle is
|
||||
saved to `"wrist_world_fallback"` in `config.json`; manual edits take effect on
|
||||
restart. This option affects wrist mounts only, not World or Head. While hidden,
|
||||
pointer controls cannot be used.
|
||||
The saved mount preference is not replaced by the fallback. These offsets and sizes are
|
||||
initial choices, **not headset-comfort acceptance**.
|
||||
|
||||
A selection saves only the mount token to `$XDG_CONFIG_HOME/frameyap/mount`
|
||||
|
||||
@@ -59,6 +59,7 @@ CONFIG_DEFAULTS = {
|
||||
"close_mic_when_idle": False,
|
||||
"backend": "redux",
|
||||
"lock_layout": False,
|
||||
"wrist_world_fallback": True,
|
||||
"clock_24h": False,
|
||||
"date_format": "mdy",
|
||||
"quick_inputs": ["/new", "/questions", "/help"],
|
||||
@@ -167,6 +168,8 @@ def normalized_config(data):
|
||||
fixed["backend"] = backend if isinstance(backend, str) and BACKEND_RE.fullmatch(backend) else "redux"
|
||||
layout = data.get("lock_layout", False)
|
||||
fixed["lock_layout"] = layout if type(layout) is bool else False
|
||||
fallback = data.get("wrist_world_fallback", True)
|
||||
fixed["wrist_world_fallback"] = fallback if type(fallback) is bool else True
|
||||
clock = data.get("clock_24h", False)
|
||||
fixed["clock_24h"] = clock if type(clock) is bool else False
|
||||
date = data.get("date_format", "mdy")
|
||||
|
||||
@@ -39,6 +39,7 @@ CONFIG_DEFAULTS = {
|
||||
"close_mic_when_idle": False,
|
||||
"backend": "redux",
|
||||
"lock_layout": False,
|
||||
"wrist_world_fallback": True,
|
||||
"clock_24h": False,
|
||||
"date_format": "mdy",
|
||||
"quick_inputs": ["/new", "/questions", "/help"],
|
||||
@@ -147,6 +148,8 @@ def normalized_config(data):
|
||||
fixed["backend"] = backend if isinstance(backend, str) and BACKEND_RE.fullmatch(backend) else "redux"
|
||||
layout = data.get("lock_layout", False)
|
||||
fixed["lock_layout"] = layout if type(layout) is bool else False
|
||||
fallback = data.get("wrist_world_fallback", True)
|
||||
fixed["wrist_world_fallback"] = fallback if type(fallback) is bool else True
|
||||
clock = data.get("clock_24h", False)
|
||||
fixed["clock_24h"] = clock if type(clock) is bool else False
|
||||
date = data.get("date_format", "mdy")
|
||||
|
||||
@@ -215,6 +215,9 @@ Config load_config(const std::filesystem::path& path) {
|
||||
} else if (key == "lock_layout") {
|
||||
if (!value.is_bool) throw std::runtime_error("Config lock_layout must be a boolean");
|
||||
config.lock_layout = value.value == "true";
|
||||
} else if (key == "wrist_world_fallback") {
|
||||
if (!value.is_bool) throw std::runtime_error("Config wrist_world_fallback must be a boolean");
|
||||
config.wrist_world_fallback = value.value == "true";
|
||||
} else if (key == "clock_24h") {
|
||||
if (!value.is_bool) throw std::runtime_error("Config clock_24h must be a boolean");
|
||||
config.clock_24h = value.value == "true";
|
||||
@@ -351,6 +354,9 @@ bool save_close_mic_when_idle(const std::filesystem::path& path, bool enabled) n
|
||||
bool save_lock_layout(const std::filesystem::path& path, bool enabled) noexcept {
|
||||
return save_bool_option(path, "lock_layout", enabled);
|
||||
}
|
||||
bool save_wrist_world_fallback(const std::filesystem::path& path, bool enabled) noexcept {
|
||||
return save_bool_option(path, "wrist_world_fallback", enabled);
|
||||
}
|
||||
bool save_clock_24h(const std::filesystem::path& path, bool enabled) noexcept {
|
||||
return save_bool_option(path, "clock_24h", enabled);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ struct Config {
|
||||
bool auto_insert = false; // opt-in; runtime also requires uninterrupted verified Xwayland focus
|
||||
bool close_mic_when_idle = false; // default keeps the device open, discarding idle audio
|
||||
bool lock_layout = false; // hide the grab and scale handles when enabled
|
||||
bool wrist_world_fallback = true; // show a world-space panel if a selected wrist loses tracking
|
||||
bool clock_24h = false;
|
||||
DateFormat date_format = DateFormat::MonthDayYear;
|
||||
WristPlacement wrist;
|
||||
@@ -46,6 +47,7 @@ bool save_advanced_debug(const std::filesystem::path& path, bool enabled) noexce
|
||||
bool save_auto_insert(const std::filesystem::path& path, bool enabled) noexcept;
|
||||
bool save_close_mic_when_idle(const std::filesystem::path& path, bool enabled) noexcept;
|
||||
bool save_lock_layout(const std::filesystem::path& path, bool enabled) noexcept;
|
||||
bool save_wrist_world_fallback(const std::filesystem::path& path, bool enabled) noexcept;
|
||||
bool save_clock_24h(const std::filesystem::path& path, bool enabled) noexcept;
|
||||
bool save_date_format(const std::filesystem::path& path, DateFormat format) noexcept;
|
||||
bool save_backend(const std::filesystem::path& path, const std::string& id) noexcept;
|
||||
|
||||
@@ -38,6 +38,13 @@ std::optional<Mount> parse_mount(std::string_view name) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Mount> effective_mount(Mount selected, bool wrist_tracked, bool wrist_world_fallback) {
|
||||
if (selected != Mount::LeftWrist && selected != Mount::RightWrist) return selected;
|
||||
if (wrist_tracked) return selected;
|
||||
if (wrist_world_fallback) return Mount::World;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Matrix34> world_mount_pose(const Matrix34& hmd) {
|
||||
for (const auto& row : hmd)
|
||||
for (float value : row)
|
||||
|
||||
@@ -11,6 +11,8 @@ enum class Mount { World, LeftWrist, RightWrist, Head };
|
||||
|
||||
std::string_view mount_name(Mount mount);
|
||||
std::optional<Mount> parse_mount(std::string_view name);
|
||||
// No mount means hide the panel until the selected wrist is tracked again.
|
||||
std::optional<Mount> effective_mount(Mount selected, bool wrist_tracked, bool wrist_world_fallback);
|
||||
|
||||
// Row-major 3x4 affine transform (OpenVR axes: +Y up, -Z forward).
|
||||
using Matrix34 = std::array<std::array<float, 4>, 3>;
|
||||
|
||||
+45
-7
@@ -19,6 +19,7 @@
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -118,6 +119,7 @@ struct Overlay::Impl {
|
||||
std::array<vr::TrackedDevicePose_t, vr::k_unMaxTrackedDeviceCount> poses{};
|
||||
bool grip_capture = false, ptt_capture = false;
|
||||
bool focus = true;
|
||||
std::array<std::optional<std::pair<float, float>>, 2> cursor_positions{};
|
||||
struct Diagnostics {
|
||||
vr::EVRInputError action_update_error = vr::VRInputError_None;
|
||||
unsigned pointer_downs = 0, pointer_ups = 0, pointer_actions = 0, pointer_resets = 0;
|
||||
@@ -182,7 +184,12 @@ struct Overlay::Impl {
|
||||
lasers_anytime = false;
|
||||
persistence.laser_change_failed = true;
|
||||
}
|
||||
overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true), overlay, "Laser scroll");
|
||||
// Smooth events are optional on some runtimes; discrete wheel events
|
||||
// keep Settings usable if the compositor declines the smooth mode.
|
||||
overlay->SetOverlayFlag(handle, vr::VROverlayFlags_SendVRSmoothScrollEvents, true);
|
||||
surface.set_lasers_anytime(lasers_anytime);
|
||||
surface.set_wrist_world_fallback(config.wrist_world_fallback);
|
||||
surface.set_advanced_debug(config.advanced_debug);
|
||||
surface.set_auto_insert(config.auto_insert);
|
||||
surface.set_close_mic_when_idle(config.close_mic_when_idle);
|
||||
@@ -248,14 +255,22 @@ struct Overlay::Impl {
|
||||
shown = wanted;
|
||||
}
|
||||
void place() {
|
||||
Mount effective = mount;
|
||||
vr::TrackedDeviceIndex_t target = vr::k_unTrackedDeviceIndex_Hmd;
|
||||
if (mount == Mount::LeftWrist || mount == Mount::RightWrist) {
|
||||
target = system->GetTrackedDeviceIndexForControllerRole(mount == Mount::LeftWrist
|
||||
? vr::TrackedControllerRole_LeftHand : vr::TrackedControllerRole_RightHand);
|
||||
if (target >= poses.size() || !poses[target].bPoseIsValid || !system->IsTrackedDeviceConnected(target))
|
||||
effective = Mount::World;
|
||||
}
|
||||
const bool wrist_tracked = target < poses.size() && poses[target].bPoseIsValid &&
|
||||
system->IsTrackedDeviceConnected(target);
|
||||
const auto selected = effective_mount(mount, wrist_tracked, config.wrist_world_fallback);
|
||||
if (!selected) {
|
||||
if (placed) { surface.reset_pointers(); dragging.panel.reset(); }
|
||||
placed = false;
|
||||
applied_mount.reset();
|
||||
visibility();
|
||||
return;
|
||||
}
|
||||
const Mount effective = *selected;
|
||||
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 = persistence.mic_save_failed ? "Mic preference not saved; using it only for this session." :
|
||||
@@ -488,7 +503,7 @@ struct Overlay::Impl {
|
||||
result.push_back(UiAction::Quit); break;
|
||||
case vr::VREvent_OverlayHidden:
|
||||
++diagnostics.overlay_hidden_events;
|
||||
focus = false; reset_input(result);
|
||||
focus = false; cursor_positions.fill(std::nullopt); reset_input(result);
|
||||
if (panel.recording) result.push_back(UiAction::Cancel);
|
||||
break;
|
||||
case vr::VREvent_OverlayShown:
|
||||
@@ -497,6 +512,7 @@ struct Overlay::Impl {
|
||||
case vr::VREvent_ImageLoaded: ++diagnostics.image_loaded_events; break;
|
||||
case vr::VREvent_ImageFailed: ++diagnostics.image_failed_events; break;
|
||||
case vr::VREvent_OverlayGamepadFocusLost:
|
||||
cursor_positions.fill(std::nullopt);
|
||||
surface.reset_pointers(); ++diagnostics.pointer_resets;
|
||||
diagnostics.last_pointer_event = "gamepad focus lost"; break;
|
||||
case vr::VREvent_OverlayFocusChanged:
|
||||
@@ -506,10 +522,21 @@ struct Overlay::Impl {
|
||||
// a release must still hit the same enabled control.
|
||||
diagnostics.last_pointer_event = "overlay focus changed"; break;
|
||||
case vr::VREvent_MouseMove:
|
||||
// Manipulation uses the captured controller ray, not coordinates
|
||||
// fed back from a changing overlay or a batch of stale mouse hits.
|
||||
// Hover is for scrolling only. Manipulation still uses the captured
|
||||
// controller ray, never mouse coordinates from a changing overlay.
|
||||
if (event.data.mouse.cursorIndex < cursor_positions.size())
|
||||
cursor_positions[event.data.mouse.cursorIndex] = {event.data.mouse.x, H - event.data.mouse.y};
|
||||
break;
|
||||
case vr::VREvent_ScrollDiscrete: case vr::VREvent_ScrollSmooth:
|
||||
if (event.data.scroll.cursorIndex < cursor_positions.size() &&
|
||||
cursor_positions[event.data.scroll.cursorIndex]) {
|
||||
const auto [x, y] = *cursor_positions[event.data.scroll.cursorIndex];
|
||||
surface.scroll_settings(x, y, event.data.scroll.ydelta);
|
||||
}
|
||||
break;
|
||||
case vr::VREvent_MouseButtonDown:
|
||||
if (event.data.mouse.cursorIndex < cursor_positions.size())
|
||||
cursor_positions[event.data.mouse.cursorIndex] = {event.data.mouse.x, H - event.data.mouse.y};
|
||||
++diagnostics.pointer_downs;
|
||||
diagnostics.last_pointer_event = "down button=" + std::to_string(event.data.mouse.button);
|
||||
if (event.data.mouse.button == vr::VRMouseButton_Left)
|
||||
@@ -517,11 +544,13 @@ struct Overlay::Impl {
|
||||
begin_drag(*kind, event);
|
||||
break;
|
||||
case vr::VREvent_MouseButtonUp:
|
||||
if (event.data.mouse.cursorIndex < cursor_positions.size())
|
||||
cursor_positions[event.data.mouse.cursorIndex] = {event.data.mouse.x, H - event.data.mouse.y};
|
||||
++diagnostics.pointer_ups;
|
||||
diagnostics.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 || event_result.lasers_anytime || event_result.open_bindings || event_result.advanced_debug || event_result.auto_insert || event_result.close_mic_when_idle || event_result.lock_layout || event_result.clock_24h || event_result.date_format || event_result.model_action) ++diagnostics.pointer_actions;
|
||||
if (event_result.action || event_result.mount || event_result.recenter || event_result.lasers_anytime || event_result.open_bindings || event_result.advanced_debug || event_result.auto_insert || event_result.close_mic_when_idle || event_result.lock_layout || event_result.wrist_world_fallback || event_result.clock_24h || event_result.date_format || event_result.model_action) ++diagnostics.pointer_actions;
|
||||
if (event_result.action) result.push_back(*event_result.action);
|
||||
if (event_result.model_action) {
|
||||
model_actions.push_back(*event_result.model_action);
|
||||
@@ -550,6 +579,15 @@ struct Overlay::Impl {
|
||||
reset_input(result); dragging.panel.reset();
|
||||
return result;
|
||||
}
|
||||
if (event_result.wrist_world_fallback) {
|
||||
config.wrist_world_fallback = *event_result.wrist_world_fallback;
|
||||
persistence.save_failed = persistence.persist_mount &&
|
||||
!save_wrist_world_fallback(default_config_path(), config.wrist_world_fallback);
|
||||
surface.set_wrist_world_fallback(config.wrist_world_fallback);
|
||||
reset_input(result);
|
||||
place(); // hide or show immediately if the wrist is currently untracked
|
||||
return result;
|
||||
}
|
||||
if (event_result.close_mic_when_idle) {
|
||||
config.close_mic_when_idle = *event_result.close_mic_when_idle;
|
||||
persistence.mic_save_failed = persistence.persist_mount &&
|
||||
|
||||
+69
-19
@@ -31,10 +31,10 @@ struct Rect {
|
||||
enum class Control { Review, Settings, Bindings, Prev, Next, Record, Cancel, Insert, Enter, Quit,
|
||||
World, Left, Right, Head, Recenter, LasersAnytime, AdvancedDebug, AutoInsert,
|
||||
Clock24h, Date, LockLayout, CloseMicWhenIdle, Models, ModelRow,
|
||||
ModelInstall, ModelPrev, ModelNext, About };
|
||||
ModelInstall, ModelPrev, ModelNext, About, WristWorldFallback };
|
||||
enum class Tab { Review, Settings, Models, About };
|
||||
struct Button { Rect r; Control id; const char* label; };
|
||||
constexpr std::array<Button, 33> buttons{{
|
||||
constexpr std::array<Button, 34> buttons{{
|
||||
{{32, 138, 180, 46}, Control::Review, "Review"},
|
||||
{{226, 138, 180, 46}, Control::Settings, "Settings"},
|
||||
{{420, 138, 180, 46}, Control::Bindings, "Bindings"},
|
||||
@@ -68,7 +68,10 @@ constexpr std::array<Button, 33> buttons{{
|
||||
{{32, 530, 214, 36}, Control::ModelPrev, "Previous"},
|
||||
{{260, 530, 214, 36}, Control::ModelNext, "Next"},
|
||||
{{346, 378, 154, 44}, Control::About, "About"},
|
||||
{{32, 584, 936, 50}, Control::WristWorldFallback, "Wrist world fallback"},
|
||||
}};
|
||||
constexpr Rect settings_view{32, 232, 936, 316};
|
||||
constexpr int settings_max_scroll = 634 - (settings_view.y + settings_view.h);
|
||||
std::optional<UiAction> action(Control c) {
|
||||
switch (c) {
|
||||
case Control::Record: return UiAction::Record;
|
||||
@@ -88,6 +91,12 @@ std::optional<Mount> mounting(Control c) {
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
bool settings_control(Control c) {
|
||||
return c == Control::Models || c == Control::About || mounting(c) ||
|
||||
c == Control::Recenter || c == Control::LasersAnytime || c == Control::AdvancedDebug ||
|
||||
c == Control::AutoInsert || c == Control::Clock24h || c == Control::Date ||
|
||||
c == Control::CloseMicWhenIdle || c == Control::WristWorldFallback;
|
||||
}
|
||||
// Invalid bytes become visible replacement glyphs, never control commands.
|
||||
uint32_t next_codepoint(std::string_view s, size_t& i) {
|
||||
const auto a = static_cast<unsigned char>(s[i++]);
|
||||
@@ -122,8 +131,10 @@ struct PanelSurface::Impl {
|
||||
Color background, card, ink, muted, cyan, pink;
|
||||
Tab tab = Tab::Review;
|
||||
bool dirty = true, lasers_anytime = false, advanced_debug = false, auto_insert = false;
|
||||
bool close_mic_when_idle = false;
|
||||
bool close_mic_when_idle = false, wrist_world_fallback = true;
|
||||
bool clock_24h = false, layout_locked = false;
|
||||
float settings_scroll = 0.f;
|
||||
bool clip_settings = false;
|
||||
DateFormat date_format = DateFormat::MonthDayYear;
|
||||
std::time_t clock_time = std::time(nullptr);
|
||||
ClockLabel displayed_clock;
|
||||
@@ -192,11 +203,15 @@ struct PanelSurface::Impl {
|
||||
return result;
|
||||
}
|
||||
void rect(Rect r, Color c) {
|
||||
for (int y = std::max(r.y, 0); y < std::min(H, r.y + r.h); ++y)
|
||||
for (int x = std::max(r.x, 0); x < std::min(W, r.x + r.w); ++x)
|
||||
for (int y = std::max(r.y, clip_settings ? settings_view.y : 0);
|
||||
y < std::min(clip_settings ? settings_view.y + settings_view.h : H, r.y + r.h); ++y)
|
||||
for (int x = std::max(r.x, clip_settings ? settings_view.x : 0);
|
||||
x < std::min(clip_settings ? settings_view.x + settings_view.w : W, r.x + r.w); ++x)
|
||||
std::copy(c.begin(), c.end(), pixels.begin() + (size_t(y) * W + x) * 4);
|
||||
}
|
||||
void blend(int x, int y, Color color, float amount) {
|
||||
if (clip_settings && (x < settings_view.x || x >= settings_view.x + settings_view.w ||
|
||||
y < settings_view.y || y >= settings_view.y + settings_view.h)) return;
|
||||
auto* dst = pixels.data() + (size_t(y) * W + x) * 4;
|
||||
// Straight-alpha source-over, including strokes in the transparent margin.
|
||||
const float alpha = amount * color[3] / 255.f;
|
||||
@@ -334,7 +349,9 @@ struct PanelSurface::Impl {
|
||||
for (unsigned row = 0; row < b.rows; ++row)
|
||||
for (unsigned col = 0; col < b.width; ++col) {
|
||||
int xx = x + glyph->bitmap_left + int(col), yy = baseline - glyph->bitmap_top + int(row);
|
||||
if (xx < 0 || xx >= right || xx >= W || yy < 0 || yy >= H) continue;
|
||||
if (xx < 0 || xx >= right || xx >= W || yy < 0 || yy >= H ||
|
||||
(clip_settings && (xx < settings_view.x || xx >= settings_view.x + settings_view.w ||
|
||||
yy < settings_view.y || yy >= settings_view.y + settings_view.h))) continue;
|
||||
unsigned char alpha = b.buffer[int(row) * b.pitch + int(col)];
|
||||
auto* dst = pixels.data() + (size_t(yy) * W + xx) * 4;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
@@ -355,7 +372,7 @@ struct PanelSurface::Impl {
|
||||
}
|
||||
bool visible(Control c) const {
|
||||
if (panel.quick_open && c != Control::Cancel && c != Control::Enter && c != Control::Quit) return false;
|
||||
if (c == Control::Models || c == Control::About) return tab == Tab::Settings;
|
||||
if (c == Control::Models || c == Control::About || c == Control::WristWorldFallback) return tab == Tab::Settings;
|
||||
if (c == Control::ModelRow) return tab == Tab::Models && !install_confirm;
|
||||
if (c == Control::ModelPrev || c == Control::ModelNext) return tab == Tab::Models;
|
||||
if (c == Control::ModelInstall) return tab == Tab::Models;
|
||||
@@ -385,10 +402,16 @@ struct PanelSurface::Impl {
|
||||
(model_page + 1) * 6 < panel.models.size();
|
||||
return true;
|
||||
}
|
||||
Rect button_rect(const Button& b) const {
|
||||
auto r = b.r;
|
||||
if (settings_control(b.id)) r.y -= int(std::lround(settings_scroll));
|
||||
return r;
|
||||
}
|
||||
int hit(float x, float y) const {
|
||||
for (size_t i = 0; i < buttons.size(); ++i) {
|
||||
if (buttons[i].id == Control::ModelRow && model_page * 6 + (i - 23) >= panel.models.size()) continue;
|
||||
if (visible(buttons[i].id) && enabled(buttons[i].id) && buttons[i].r.contains(x, y)) return int(i);
|
||||
if (settings_control(buttons[i].id) && !settings_view.contains(x, y)) continue;
|
||||
if (visible(buttons[i].id) && enabled(buttons[i].id) && button_rect(buttons[i]).contains(x, y)) return int(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -536,8 +559,10 @@ struct PanelSurface::Impl {
|
||||
for (size_t i = 0; i < buttons.size(); ++i) {
|
||||
const auto& b = buttons[i];
|
||||
if (!visible(b.id)) continue;
|
||||
bool on = enabled(b.id);
|
||||
if (b.id == Control::ModelRow && model_page * 6 + (i - 23) >= panel.models.size()) continue;
|
||||
const Rect r = button_rect(b);
|
||||
clip_settings = settings_control(b.id);
|
||||
bool on = enabled(b.id);
|
||||
bool selected = (b.id == Control::Review && tab == Tab::Review) ||
|
||||
(b.id == Control::Settings && tab == Tab::Settings) ||
|
||||
(mounting(b.id) && *mounting(b.id) == mount) ||
|
||||
@@ -551,14 +576,14 @@ struct PanelSurface::Impl {
|
||||
const Color accent = b.id == Control::Record && panel.recording ? pink : cyan;
|
||||
const bool highlighted = on && (selected || b.id == Control::Record ||
|
||||
(b.id == Control::Insert && panel.transcript.size()));
|
||||
rounded(b.r, std::min(16, b.r.h / 3), fill,
|
||||
rounded(r, std::min(16, r.h / 3), fill,
|
||||
!on ? mix(card, muted, .13f) : highlighted ? accent : mix(card, muted, .38f),
|
||||
highlighted ? .23f : 0.f, highlighted ? 2 : 1);
|
||||
if (b.id == Control::Quit && (pressed[0] == int(i) || pressed[1] == int(i))) {
|
||||
const int cursor = pressed[0] == int(i) ? 0 : 1;
|
||||
const float fraction = std::clamp(float(std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
PanelSurface::Clock::now() - press_time[cursor]).count()) / PanelSurface::quit_hold.count(), 0.f, 1.f);
|
||||
rect({b.r.x + 7, b.r.y + b.r.h - 9, int((b.r.w - 14) * fraction), 3}, pink);
|
||||
rect({r.x + 7, r.y + r.h - 9, int((r.w - 14) * fraction), 3}, pink);
|
||||
}
|
||||
const std::string label = b.id == Control::ModelRow ? [&]() {
|
||||
const auto& m = panel.models[model_page * 6 + i - 23];
|
||||
@@ -571,23 +596,31 @@ struct PanelSurface::Impl {
|
||||
b.id == Control::Date ? (date_format == DateFormat::Off ? "Date: Off" :
|
||||
date_format == DateFormat::MonthDayYear ? "Date: MM/DD/YYYY" :
|
||||
date_format == DateFormat::DayMonthYear ? "Date: DD/MM/YYYY" : "Date: YYYY-MM-DD") : b.label;
|
||||
text(label, b.r.x + 16, b.r.y + b.r.h / 2 + 9,
|
||||
text(label, r.x + 16, r.y + r.h / 2 + 9,
|
||||
b.id == Control::Date ? 23 : b.id == Control::CloseMicWhenIdle ? 22 :
|
||||
b.id == Control::Enter ? 20 : 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);
|
||||
on ? ink : mix(background, muted, .48f), r.x + r.w - 8);
|
||||
if (mounting(b.id) && selected) text("ON", r.x + r.w - 56, r.y + 38, 23, cyan, r.x + r.w - 12);
|
||||
if (b.id == Control::LasersAnytime || b.id == Control::AdvancedDebug || b.id == Control::AutoInsert ||
|
||||
b.id == Control::LockLayout || b.id == Control::CloseMicWhenIdle) {
|
||||
b.id == Control::LockLayout || b.id == Control::CloseMicWhenIdle || b.id == Control::WristWorldFallback) {
|
||||
bool active = b.id == Control::LasersAnytime ? lasers_anytime :
|
||||
b.id == Control::AutoInsert ? auto_insert :
|
||||
b.id == Control::LockLayout ? layout_locked :
|
||||
b.id == Control::CloseMicWhenIdle ? close_mic_when_idle : advanced_debug;
|
||||
text(active ? "ON" : "OFF", b.r.x + b.r.w - 66, b.r.y + b.r.h / 2 + 9, 22,
|
||||
active ? cyan : muted, b.r.x + b.r.w - 12);
|
||||
b.id == Control::CloseMicWhenIdle ? close_mic_when_idle :
|
||||
b.id == Control::WristWorldFallback ? wrist_world_fallback : advanced_debug;
|
||||
text(active ? "ON" : "OFF", r.x + r.w - 66, r.y + r.h / 2 + 9, 22,
|
||||
active ? cyan : muted, r.x + r.w - 12);
|
||||
}
|
||||
clip_settings = false;
|
||||
}
|
||||
if (tab == Tab::Settings)
|
||||
if (tab == Tab::Settings) {
|
||||
rect({976, settings_view.y, 3, settings_view.h}, mix(card, muted, .24f));
|
||||
const int thumb = settings_view.h * settings_view.h / (settings_view.h + settings_max_scroll);
|
||||
rect({975, settings_view.y + int(settings_scroll * (settings_view.h - thumb) / settings_max_scroll),
|
||||
5, thumb}, cyan);
|
||||
text("Right stick: scroll", 32, 565, 16, muted, 320);
|
||||
text("OFF: discard idle audio; ON: spike / start latency.", 390, 565, 16, muted, 887);
|
||||
}
|
||||
if (tab == Tab::Models && !panel.model_note.empty()) {
|
||||
// The install/navigation controls occupy y=530..566; the status
|
||||
// belongs BELOW the shared footer (574..642), not under buttons.
|
||||
@@ -664,6 +697,7 @@ SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y, Clock::
|
||||
else if (c == Control::AutoInsert) result.auto_insert = !impl_->auto_insert;
|
||||
else if (c == Control::CloseMicWhenIdle) result.close_mic_when_idle = !impl_->close_mic_when_idle;
|
||||
else if (c == Control::LockLayout) result.lock_layout = !impl_->layout_locked;
|
||||
else if (c == Control::WristWorldFallback) result.wrist_world_fallback = !impl_->wrist_world_fallback;
|
||||
else if (c == Control::Clock24h) result.clock_24h = !impl_->clock_24h;
|
||||
else if (c == Control::Date) result.date_format = static_cast<DateFormat>((static_cast<int>(impl_->date_format) + 1) % 4);
|
||||
else if (c == Control::Bindings) { result.open_bindings = true; impl_->reset(); }
|
||||
@@ -708,6 +742,17 @@ SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y, Clock::
|
||||
else if (c == Control::Next) { ++impl_->page; impl_->dirty = true; }
|
||||
return result;
|
||||
}
|
||||
bool PanelSurface::scroll_settings(float x, float y, float vertical_delta) {
|
||||
if (impl_->tab != Tab::Settings || impl_->drag_cursor >= 0 ||
|
||||
!settings_view.contains(x, y) || !std::isfinite(vertical_delta)) return false;
|
||||
const float next = std::clamp(impl_->settings_scroll - std::clamp(vertical_delta, -4.f, 4.f) * 48.f,
|
||||
0.f, float(settings_max_scroll));
|
||||
if (next == impl_->settings_scroll) return false;
|
||||
impl_->settings_scroll = next;
|
||||
impl_->reset(); // a press must not turn into a click on a different setting
|
||||
impl_->dirty = true;
|
||||
return true;
|
||||
}
|
||||
void PanelSurface::set_indicators(const StatusIndicators& indicators) {
|
||||
if (impl_->indicators != indicators) { impl_->indicators = indicators; impl_->dirty = true; }
|
||||
}
|
||||
@@ -720,6 +765,11 @@ std::vector<PanelSurface::Bounds> PanelSurface::input_regions() const {
|
||||
if (!impl_->layout_locked) { result.push_back(grab); result.push_back(scale); }
|
||||
return result;
|
||||
}
|
||||
void PanelSurface::set_wrist_world_fallback(bool enabled) {
|
||||
if (impl_->wrist_world_fallback != enabled) {
|
||||
impl_->wrist_world_fallback = enabled; impl_->reset(); impl_->dirty = true;
|
||||
}
|
||||
}
|
||||
void PanelSurface::set_layout_locked(bool locked) {
|
||||
if (impl_->layout_locked != locked) {
|
||||
impl_->layout_locked = locked; impl_->reset(); impl_->dirty = true;
|
||||
|
||||
@@ -19,6 +19,7 @@ struct SurfaceEvent {
|
||||
std::optional<bool> auto_insert;
|
||||
std::optional<bool> close_mic_when_idle;
|
||||
std::optional<bool> lock_layout;
|
||||
std::optional<bool> wrist_world_fallback;
|
||||
std::optional<bool> clock_24h;
|
||||
std::optional<DateFormat> date_format;
|
||||
bool recenter = false;
|
||||
@@ -53,10 +54,13 @@ public:
|
||||
static constexpr auto quit_hold = std::chrono::milliseconds(900);
|
||||
std::optional<PanelDragKind> pointer_down(unsigned cursor, float x, float y, Clock::time_point now = Clock::now());
|
||||
SurfaceEvent pointer_up(unsigned cursor, float x, float y, Clock::time_point now = Clock::now());
|
||||
// OpenVR laser wheel input at the last hovered panel coordinate; no paging.
|
||||
bool scroll_settings(float x, float y, float vertical_delta);
|
||||
bool dragging(unsigned cursor) const;
|
||||
// OpenVR intersection masks use top-left coordinates, unlike mouse events.
|
||||
std::vector<Bounds> input_regions() const;
|
||||
void set_layout_locked(bool locked);
|
||||
void set_wrist_world_fallback(bool enabled);
|
||||
void reset_pointers();
|
||||
void set_placement_note(std::string note);
|
||||
// Shown on Settings -> About; empty shows "version unknown".
|
||||
|
||||
@@ -46,6 +46,7 @@ int main(int argc, char** argv) {
|
||||
assert(!load_config(path).auto_insert);
|
||||
assert(!load_config(path).close_mic_when_idle);
|
||||
assert(!load_config(path).lock_layout);
|
||||
assert(load_config(path).wrist_world_fallback);
|
||||
assert(!load_config(path).clock_24h);
|
||||
assert(load_config(path).date_format == DateFormat::MonthDayYear);
|
||||
assert(load_config(path).wrist.width == .30f);
|
||||
@@ -63,6 +64,7 @@ int main(int argc, char** argv) {
|
||||
assert(!example.auto_insert);
|
||||
assert(!example.close_mic_when_idle);
|
||||
assert(!example.lock_layout);
|
||||
assert(example.wrist_world_fallback);
|
||||
assert(!example.clock_24h && example.date_format == DateFormat::MonthDayYear);
|
||||
assert(example.wrist.y == .18f && example.wrist.z == .089f);
|
||||
assert(example.gradient.enabled && example.gradient.period_seconds == 30.f && example.gradient.strength == .12f);
|
||||
@@ -190,6 +192,20 @@ int main(int argc, char** argv) {
|
||||
assert(save_lock_layout(path, true));
|
||||
assert(get(path) == R"({"font":"escaped \u0061","theme":{"ink":"#123ABC"},"auto_insert":true,"lock_layout":true})");
|
||||
assert(load_config(path).lock_layout && load_config(path).auto_insert);
|
||||
put(path, R"({"wrist_world_fallback":false})");
|
||||
assert(!load_config(path).wrist_world_fallback);
|
||||
assert(save_wrist_world_fallback(path, true));
|
||||
assert(load_config(path).wrist_world_fallback);
|
||||
assert(save_wrist_world_fallback(path, false));
|
||||
assert(!load_config(path).wrist_world_fallback);
|
||||
put(path, R"({"wrist_world_fallback":true})");
|
||||
assert(load_config(path).wrist_world_fallback);
|
||||
for (const auto* invalid : {R"({"wrist_world_fallback":0})", R"({"wrist_world_fallback":"false"})",
|
||||
R"({"wrist_world_fallback":null})"}) {
|
||||
put(path, invalid); fails([&] { load_config(path); });
|
||||
assert(!save_wrist_world_fallback(path, false));
|
||||
assert(get(path) == invalid);
|
||||
}
|
||||
put(path, R"({"input_priority":"normal"})");
|
||||
assert(!load_config(path).experimental_input_priority);
|
||||
for (const auto* invalid : {R"({"input_priority":true})", R"({"input_priority":16777216})",
|
||||
|
||||
@@ -58,6 +58,15 @@ int main() {
|
||||
}
|
||||
CHECK(!parse_mount("World")); CHECK(!parse_mount("head\n")); CHECK(!parse_mount("left_wrist"));
|
||||
CHECK(!parse_mount("")); CHECK(!parse_mount("world extra"));
|
||||
for (auto wrist : {Mount::LeftWrist, Mount::RightWrist}) {
|
||||
CHECK(effective_mount(wrist, true, true) == wrist);
|
||||
CHECK(effective_mount(wrist, true, false) == wrist);
|
||||
CHECK(effective_mount(wrist, false, true) == Mount::World);
|
||||
CHECK(!effective_mount(wrist, false, false));
|
||||
CHECK(effective_mount(wrist, true, false) == wrist); // restored tracking
|
||||
}
|
||||
CHECK(effective_mount(Mount::World, false, false) == Mount::World);
|
||||
CHECK(effective_mount(Mount::Head, false, false) == Mount::Head);
|
||||
|
||||
CHECK(near(mount_width(Mount::World), .85f)); CHECK(near(mount_width(Mount::Head), .85f));
|
||||
CHECK(near(mount_width(Mount::LeftWrist), .30f)); CHECK(near(mount_width(Mount::RightWrist), .30f));
|
||||
|
||||
+26
-1
@@ -14,7 +14,7 @@ SurfaceEvent click(PanelSurface& surface, float x, float y, unsigned cursor = 0)
|
||||
}
|
||||
void no_action(const SurfaceEvent& event) {
|
||||
assert(!event.action && !event.mount && !event.lasers_anytime && !event.advanced_debug && !event.auto_insert &&
|
||||
!event.close_mic_when_idle && !event.lock_layout && !event.clock_24h && !event.date_format && !event.recenter && !event.open_bindings && !event.model_action);
|
||||
!event.close_mic_when_idle && !event.lock_layout && !event.wrist_world_fallback && !event.clock_24h && !event.date_format && !event.recenter && !event.open_bindings && !event.model_action);
|
||||
}
|
||||
void snapshot(PanelSurface& surface, const std::string& path) {
|
||||
std::ofstream out(path, std::ios::binary);
|
||||
@@ -327,6 +327,30 @@ int main(int argc, char** argv) {
|
||||
surface.set_close_mic_when_idle(true); assert(surface.render(p));
|
||||
assert(click(surface, 200, 532).close_mic_when_idle == false);
|
||||
surface.set_close_mic_when_idle(false); assert(surface.render(p));
|
||||
// The Settings viewport scrolls continuously under the laser; it never pages.
|
||||
assert(!surface.scroll_settings(290, 160, -1.f)); // tab/header, not settings content
|
||||
assert(!surface.scroll_settings(200, 400, std::numeric_limits<float>::quiet_NaN()));
|
||||
surface.pointer_down(0, 200, 532); // pending mic press must not activate after movement
|
||||
assert(surface.scroll_settings(200, 400, -.5f));
|
||||
no_action(surface.pointer_up(0, 200, 532));
|
||||
assert(surface.render(p));
|
||||
assert(!surface.scroll_settings(200, 610, -1.f)); // fixed footer
|
||||
assert(surface.scroll_settings(200, 400, -2.f));
|
||||
assert(surface.render(p));
|
||||
assert(!surface.scroll_settings(200, 400, -1.f)); // bottom clamp
|
||||
no_action(click(surface, 180, 231)); // scrolled settings cannot be clicked above the viewport
|
||||
auto fallback = click(surface, 200, 520);
|
||||
assert(fallback.wrist_world_fallback == false && !fallback.action);
|
||||
assert(!surface.render(p));
|
||||
surface.set_wrist_world_fallback(false); assert(surface.render(p));
|
||||
fallback = click(surface, 200, 520);
|
||||
assert(fallback.wrist_world_fallback == true);
|
||||
surface.set_wrist_world_fallback(true); assert(surface.render(p));
|
||||
assert(click(surface, 280, 610).action == UiAction::Cancel); // footer stays fixed
|
||||
assert(surface.scroll_settings(200, 400, 4.f));
|
||||
assert(surface.render(p));
|
||||
assert(!surface.scroll_settings(200, 400, 1.f)); // top clamp
|
||||
assert(click(surface, 180, 260).mount == Mount::World);
|
||||
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));
|
||||
@@ -344,6 +368,7 @@ int main(int argc, char** argv) {
|
||||
no_action(click(surface, 200, 492)); // clock/date controls only exist on Settings
|
||||
no_action(click(surface, 680, 492));
|
||||
no_action(click(surface, 200, 532)); // mic preference only exists on Settings
|
||||
assert(!surface.scroll_settings(200, 400, -1.f)); // Review has no Settings scroll
|
||||
|
||||
// Bindings opens SteamVR directly, from either tab, without replacing review.
|
||||
surface.pointer_down(1, 480, 610);
|
||||
|
||||
@@ -89,6 +89,7 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertEqual(json.loads(config.read_text())["gradient"],
|
||||
{"enabled": True, "period_seconds": 30, "strength": 0.12})
|
||||
self.assertIs(json.loads(config.read_text())["lock_layout"], False)
|
||||
self.assertIs(json.loads(config.read_text())["wrist_world_fallback"], True)
|
||||
self.assertEqual(list(config.parent.glob("config.json.backup-*")), [])
|
||||
self.assertIn("PYTHONDONTWRITEBYTECODE=1", launcher.read_text())
|
||||
self.assertTrue(os.access(root / "versions/0.1.202609241530/runtime/bin/helper", os.X_OK))
|
||||
@@ -186,6 +187,7 @@ class InstallTests(unittest.TestCase):
|
||||
fixed["close_mic_when_idle"] = True
|
||||
fixed["backend"] = "custom_v2-1"
|
||||
fixed["lock_layout"] = True
|
||||
fixed["wrist_world_fallback"] = False
|
||||
fixed["clock_24h"] = True
|
||||
fixed["date_format"] = "iso"
|
||||
fixed["buttons"]["enter"] = "" # intentional disabling survives upgrades
|
||||
@@ -200,6 +202,7 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertIs(json.loads(config.read_text())["close_mic_when_idle"], True)
|
||||
self.assertEqual(json.loads(config.read_text())["backend"], "custom_v2-1")
|
||||
self.assertIs(json.loads(config.read_text())["lock_layout"], True)
|
||||
self.assertIs(json.loads(config.read_text())["wrist_world_fallback"], False)
|
||||
self.assertIs(json.loads(config.read_text())["clock_24h"], True)
|
||||
self.assertEqual(json.loads(config.read_text())["date_format"], "iso")
|
||||
self.assertEqual(len(list(config.parent.glob("config.json.backup-*"))), 1)
|
||||
@@ -221,6 +224,7 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertIs(fixed["advanced_debug"], False)
|
||||
self.assertIs(fixed["auto_insert"], False)
|
||||
self.assertIs(fixed["lock_layout"], False)
|
||||
self.assertIs(fixed["wrist_world_fallback"], True)
|
||||
self.assertEqual(fixed["wrist"]["x"], 0.04)
|
||||
self.assertEqual(fixed["wrist"]["y"], 0.18)
|
||||
self.assertEqual(fixed["wrist"]["width"], 0.30)
|
||||
@@ -350,6 +354,12 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertEqual(fixed["font"], "/custom/font.ttf")
|
||||
self.assertIn(original, [p.read_bytes() for p in config.parent.glob("config.json.backup-*")])
|
||||
|
||||
def test_wrist_world_fallback_normalization(self):
|
||||
self.assertIs(installer.normalized_config({})["wrist_world_fallback"], True)
|
||||
self.assertIs(installer.normalized_config({"wrist_world_fallback": False})["wrist_world_fallback"], False)
|
||||
for invalid in ("false", 0, None, [], {}):
|
||||
self.assertIs(installer.normalized_config({"wrist_world_fallback": invalid})["wrist_world_fallback"], True)
|
||||
|
||||
def test_new_config_fields_repair_only_invalid_values(self):
|
||||
archive, digest = self.package("0.1.202609241530")
|
||||
config = self.home / ".config/frameyap/config.json"
|
||||
|
||||
Reference in new issue
Block a user