Add rigid controller grab and layout lock; fix scale hit mask

This commit is contained in:
baketnk committed 2026-09-24 17:01:33 -04:00
1 parent 9c7fdae264
commit 0d4cfbfdb7
15 files changed
+368 -143

No files matched your search

+6 -4
View File
@@ -30,10 +30,12 @@ See [third-party notes](docs/third-party.md). No GitHub release is published yet
- **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. The header Review and settings share one Inconsolata/neon-framed surface. The header
shows local time/date; Settings selects 12/24-hour time and date format/off. shows local time/date; Settings selects 12/24-hour time and date format/off.
Drag the thin bar below the panel to move it in its plane; drag the external Hold the thin bar below the panel to freely move and rotate it with your
lower-right corner bracket to scale it (world, head or wrist). Both sit in controller; release to leave it at that pose. Drag the external lower-right
transparent margins, like Steam's window handles. Scaling keeps the upper-left bracket to scale it (world, head or wrist), keeping the upper-left anchored.
corner anchored. Position/size changes last for this run only. Both handles use the app's gradient in transparent margins, like Steam's
window handles. Settings → **Lock grab/scale** hides and disables both handles;
the lock is saved. Position/size changes last for this run only.
Wrist mounting fades the panel as its full orientation turns away from an Wrist mounting fades the panel as its full orientation turns away from an
upright viewer-facing pose (60°–75°), hiding interaction past that angle; upright viewer-facing pose (60°–75°), hiding interaction past that angle;
world and head mounting do not fade. world and head mounting do not fade.
+1
View File
@@ -3,6 +3,7 @@
"input_priority": "normal", "input_priority": "normal",
"advanced_debug": false, "advanced_debug": false,
"auto_insert": false, "auto_insert": false,
"lock_layout": false,
"clock_24h": false, "clock_24h": false,
"date_format": "mdy", "date_format": "mdy",
"wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0}, "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0},
+30 -14
View File
@@ -105,6 +105,7 @@ installer creates one with defaults on first install. Copy the shipped
"input_priority": "normal", "input_priority": "normal",
"advanced_debug": false, "advanced_debug": false,
"auto_insert": false, "auto_insert": false,
"lock_layout": false,
"clock_24h": false, "clock_24h": false,
"date_format": "mdy", "date_format": "mdy",
"wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0}, "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0},
@@ -223,19 +224,32 @@ lower-right corner, visually modeled on the user's Steam terminal-window screens
These are original FrameYap controls, not Steam private UI components. RGBA alpha These are original FrameYap controls, not Steam private UI components. RGBA alpha
leaves their surrounding area transparent; an explicit OpenVR intersection mask leaves their surrounding area transparent; an explicit OpenVR intersection mask
excludes empty margins from laser hit testing (the slender strokes have larger hit excludes empty margins from laser hit testing (the slender strokes have larger hit
targets). The extra canvas area does not shrink the main panel's physical width. targets). Both handles use the configured mint-to-blue frame gradient. The extra
canvas area does not shrink the main panel's physical width. Intersection rectangles
use top-left pixel coordinates; only mouse events need the bottom-left GL Y flip.
The first deployed pass incorrectly flipped the mask, leaving the scale corner
outside its input region; this version corrects that mapping.
Hold the laser's primary click on the bar and move to translate the panel **in its Hold the laser's primary click on the bar to **freely position and rotate** the
plane**; this does not change depth or orientation. Drag the corner bracket to scale panel with the controller, including depth, pitch, yaw and roll. Grab captures
between half and twice the configured width. The original upper-left corner remains `inverse(controller_down) * panel_down` and applies that unchanged relative pose
fixed relative to the chosen mount. Both work on either tab and all mounts. Movement to each controller pose, so grabbing does not snap or reset the panel orientation.
is bounded to two meters per axis; mount changes/recenter clear movement, while mount Release leaves the last pose in the chosen mount frame; head/wrist mounts continue
changes retain the size factor. Restart restores configured position/size defaults. following that anchor afterward. Mount changes/recenter reset the pose, while
retaining the size factor. Restart restores configured position/size defaults.
A drag freezes its initial plane and calibrates a ray from the source controller to Drag the corner bracket to scale between half and twice the configured width.
the initial hit. Subsequent tracked poses determine translation/scale, even beyond Scaling freezes its initial plane and calibrates a ray from the source controller
the original texture bounds; changing overlay coordinates never feed back into the to the initial hit. The top-left corner of the current, potentially rotated panel
drag. For head/wrist mounts, geometry stays in the anchor-device coordinate frame. stays fixed, including after repeated grab/scale operations. Changing overlay
coordinates never feed back into the calculation. Both controls work on either tab;
for head/wrist mounts, geometry stays in the anchor-device coordinate frame.
Settings → **Lock grab/scale** (`"lock_layout": true`, default false) hides both
handles and removes their hit regions, cancels an active drag and prevents new
manipulation. Unlock remains accessible in Settings. The boolean is saved across
restarts/upgrades; failed saves leave a visible session-only warning. It does not
disable explicit mount/recenter choices or freeze normal head/wrist tracking.
The event's controller is used, with the primary dashboard device as the single-cursor The event's controller is used, with the primary dashboard device as the single-cursor
fallback when the event omits it. No guessed controller or desktop pointer fallback. fallback when the event omits it. No guessed controller or desktop pointer fallback.
Release, changed UI authorization, tracking loss, hidden overlay, relocation, invalid Release, changed UI authorization, tracking loss, hidden overlay, relocation, invalid
@@ -304,9 +318,11 @@ coexistence remains unverified.
### Hardware-free UI checks ### Hardware-free UI checks
The default build tests mount parsing, laser preference persistence, pose geometry The default build tests mount parsing, laser preference persistence, pose geometry
and captured-ray grab/scale math without any native dependencies. Drag tests cover and controller-relative grab/captured-ray scale math without any native dependencies.
stationary stability, independent axes, rotated/relative mounts, out-of-bounds hits, Drag tests cover XYZ translation, pitch/yaw/roll and lever-arm rotation, re-grabbing
invalid rays and no feedback from prior updates. A FreeType-only opt-in build exercises the actual renderer, a moved panel, stationary stability, relative mounts, out-of-bounds scale hits,
invalid poses/rays and no feedback from prior updates. UI/config/installer tests
cover hidden/disabled handles, top-left mask regions, lock persistence and repair. A FreeType-only opt-in build exercises the actual renderer,
pointer gating, tab switches, pagination, recording state and redraw invalidation: pointer gating, tab switches, pagination, recording state and redraw invalidation:
```sh ```sh
+3
View File
@@ -36,6 +36,7 @@ CONFIG_DEFAULTS = {
"input_priority": "normal", "input_priority": "normal",
"advanced_debug": False, "advanced_debug": False,
"auto_insert": False, "auto_insert": False,
"lock_layout": False,
"clock_24h": False, "clock_24h": False,
"date_format": "mdy", "date_format": "mdy",
"wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0}, "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0},
@@ -100,6 +101,8 @@ def normalized_config(data):
fixed["advanced_debug"] = debug if type(debug) is bool else False fixed["advanced_debug"] = debug if type(debug) is bool else False
automatic = data.get("auto_insert", False) automatic = data.get("auto_insert", False)
fixed["auto_insert"] = automatic if type(automatic) is bool else False fixed["auto_insert"] = automatic if type(automatic) is bool else False
layout = data.get("lock_layout", False)
fixed["lock_layout"] = layout if type(layout) is bool else False
clock = data.get("clock_24h", False) clock = data.get("clock_24h", False)
fixed["clock_24h"] = clock if type(clock) is bool else False fixed["clock_24h"] = clock if type(clock) is bool else False
date = data.get("date_format", "mdy") date = data.get("date_format", "mdy")
+3
View File
@@ -25,6 +25,7 @@ CONFIG_DEFAULTS = {
"input_priority": "normal", "input_priority": "normal",
"advanced_debug": False, "advanced_debug": False,
"auto_insert": False, "auto_insert": False,
"lock_layout": False,
"clock_24h": False, "clock_24h": False,
"date_format": "mdy", "date_format": "mdy",
"wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0}, "wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0},
@@ -89,6 +90,8 @@ def normalized_config(data):
fixed["advanced_debug"] = debug if type(debug) is bool else False fixed["advanced_debug"] = debug if type(debug) is bool else False
automatic = data.get("auto_insert", False) automatic = data.get("auto_insert", False)
fixed["auto_insert"] = automatic if type(automatic) is bool else False fixed["auto_insert"] = automatic if type(automatic) is bool else False
layout = data.get("lock_layout", False)
fixed["lock_layout"] = layout if type(layout) is bool else False
clock = data.get("clock_24h", False) clock = data.get("clock_24h", False)
fixed["clock_24h"] = clock if type(clock) is bool else False fixed["clock_24h"] = clock if type(clock) is bool else False
date = data.get("date_format", "mdy") date = data.get("date_format", "mdy")
+6
View File
@@ -200,6 +200,9 @@ Config load_config(const std::filesystem::path& path) {
} else if (key == "auto_insert") { } else if (key == "auto_insert") {
if (!value.is_bool) throw std::runtime_error("Config auto_insert must be a boolean"); if (!value.is_bool) throw std::runtime_error("Config auto_insert must be a boolean");
config.auto_insert = value.value == "true"; config.auto_insert = value.value == "true";
} 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 == "clock_24h") { } else if (key == "clock_24h") {
if (!value.is_bool) throw std::runtime_error("Config clock_24h must be a boolean"); if (!value.is_bool) throw std::runtime_error("Config clock_24h must be a boolean");
config.clock_24h = value.value == "true"; config.clock_24h = value.value == "true";
@@ -306,6 +309,9 @@ 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_auto_insert(const std::filesystem::path& path, bool enabled) noexcept {
return save_bool_option(path, "auto_insert", enabled); return save_bool_option(path, "auto_insert", enabled);
} }
bool save_lock_layout(const std::filesystem::path& path, bool enabled) noexcept {
return save_bool_option(path, "lock_layout", enabled);
}
bool save_clock_24h(const std::filesystem::path& path, bool enabled) noexcept { bool save_clock_24h(const std::filesystem::path& path, bool enabled) noexcept {
return save_bool_option(path, "clock_24h", enabled); return save_bool_option(path, "clock_24h", enabled);
} }
+2
View File
@@ -21,6 +21,7 @@ struct Config {
bool experimental_input_priority = false; bool experimental_input_priority = false;
bool advanced_debug = false; // opt-in full diagnostic logging; never raw audio recording bool advanced_debug = false; // opt-in full diagnostic logging; never raw audio recording
bool auto_insert = false; // opt-in; runtime also requires uninterrupted verified Xwayland focus bool auto_insert = false; // opt-in; runtime also requires uninterrupted verified Xwayland focus
bool lock_layout = false; // hide the grab and scale handles when enabled
bool clock_24h = false; bool clock_24h = false;
DateFormat date_format = DateFormat::MonthDayYear; DateFormat date_format = DateFormat::MonthDayYear;
WristPlacement wrist; WristPlacement wrist;
@@ -33,6 +34,7 @@ Config load_config(const std::filesystem::path& path);
// Other user customizations and formatting are retained; creates a minimal config if absent. // Other user customizations and formatting are retained; creates a minimal config if absent.
bool save_advanced_debug(const std::filesystem::path& path, bool enabled) noexcept; bool save_advanced_debug(const std::filesystem::path& path, bool enabled) noexcept;
bool save_auto_insert(const std::filesystem::path& path, bool enabled) noexcept; bool save_auto_insert(const std::filesystem::path& path, bool enabled) noexcept;
bool save_lock_layout(const std::filesystem::path& path, bool enabled) noexcept;
bool save_clock_24h(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_date_format(const std::filesystem::path& path, DateFormat format) noexcept;
std::string resolve_font(const std::string& assets, const std::string& requested); std::string resolve_font(const std::string& assets, const std::string& requested);
+50 -41
View File
@@ -43,13 +43,6 @@ Matrix34 matrix(const vr::HmdMatrix34_t& value) {
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) result[r][c] = value.m[r][c]; for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) result[r][c] = value.m[r][c];
return result; return result;
} }
Matrix34 relative_to(const Matrix34& pose, const Matrix34& parent) {
Matrix34 result{}; // inverse rigid parent * pose
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c)
for (int k = 0; k < 3; ++k)
result[r][c] += parent[k][r] * (pose[k][c] - (c == 3 ? parent[k][3] : 0.f));
return result;
}
constexpr std::array<const char*, 6> action_names{{"left_grip", "right_grip", "ptt", "cancel", "insert", "enter"}}; constexpr std::array<const char*, 6> action_names{{"left_grip", "right_grip", "ptt", "cancel", "insert", "enter"}};
void overlay_check(vr::EVROverlayError err, vr::IVROverlay* api, const char* op) { void overlay_check(vr::EVROverlayError err, vr::IVROverlay* api, const char* op) {
if (err != vr::VROverlayError_None) if (err != vr::VROverlayError_None)
@@ -90,19 +83,18 @@ struct Overlay::Impl {
Config config; Config config;
PanelSurface surface; PanelSurface surface;
Panel panel; Panel panel;
bool save_failed = false, debug_save_failed = false, auto_save_failed = false; bool save_failed = false, debug_save_failed = false, auto_save_failed = false, layout_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 published_alpha = -1.f; float published_alpha = -1.f;
float size_scale = 1.f; float size_scale = 1.f;
bool size_changed = false; bool placement_dirty = false;
float move_x = 0.f, move_y = 0.f; Matrix34 canvas_pose{}, drag_canvas{};
Matrix34 canvas_pose{};
PanelDrag drag; PanelDrag drag;
PanelDragKind drag_kind = PanelDragKind::Grab; PanelDragKind drag_kind = PanelDragKind::Grab;
unsigned drag_cursor = 0; unsigned drag_cursor = 0;
vr::TrackedDeviceIndex_t drag_device = vr::k_unTrackedDeviceIndexInvalid; vr::TrackedDeviceIndex_t drag_device = vr::k_unTrackedDeviceIndexInvalid;
bool drag_trigger_observed = false; bool drag_trigger_observed = false;
float drag_scale = 1.f, drag_x = 0.f, drag_y = 0.f; float drag_scale = 1.f;
std::chrono::steady_clock::time_point drag_started{}; std::chrono::steady_clock::time_point drag_started{};
vr::HmdMatrix34_t world_transform{}; vr::HmdMatrix34_t world_transform{};
std::optional<Mount> applied_mount; std::optional<Mount> applied_mount;
@@ -180,21 +172,13 @@ struct Overlay::Impl {
surface.set_lasers_anytime(lasers_anytime); surface.set_lasers_anytime(lasers_anytime);
surface.set_advanced_debug(config.advanced_debug); surface.set_advanced_debug(config.advanced_debug);
surface.set_auto_insert(config.auto_insert); surface.set_auto_insert(config.auto_insert);
surface.set_layout_locked(config.lock_layout);
surface.set_clock_24h(config.clock_24h); surface.set_clock_24h(config.clock_24h);
surface.set_date_format(config.date_format); surface.set_date_format(config.date_format);
overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_VisibleInDashboard, true), overlay, "VisibleInDashboard"); overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_VisibleInDashboard, true), overlay, "VisibleInDashboard");
vr::HmdVector2_t mouse_scale{{float(W), float(H)}}; vr::HmdVector2_t mouse_scale{{float(W), float(H)}};
overlay_check(overlay->SetOverlayMouseScale(handle, &mouse_scale), overlay, "SetOverlayMouseScale"); overlay_check(overlay->SetOverlayMouseScale(handle, &mouse_scale), overlay, "SetOverlayMouseScale");
// Alpha is visual, not an input mask. Exclude the empty margins from update_intersection_mask();
// laser intersection rather than blocking neighboring windows there.
std::array<vr::VROverlayIntersectionMaskPrimitive_t, 3> mask{};
const std::array<PanelSurface::Bounds, 3> regions{{{10, 10, CW - 20, CH - 20}, PanelSurface::grab, PanelSurface::scale}};
for (size_t i = 0; i < mask.size(); ++i) {
const auto b = regions[i];
mask[i].m_nPrimitiveType = vr::OverlayIntersectionPrimitiveType_Rectangle;
mask[i].m_Primitive.m_Rectangle = {float(b.x), float(H - b.y - b.h), float(b.w), float(b.h)};
}
overlay_check(overlay->SetOverlayIntersectionMask(handle, mask.data(), uint32_t(mask.size())), overlay, "SetOverlayIntersectionMask");
system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, poses.data(), uint32_t(poses.size())); system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, poses.data(), uint32_t(poses.size()));
place(); place();
draw(Panel{"Disabled", "", "Record to start local worker", false, false}); draw(Panel{"Disabled", "", "Record to start local worker", false, false});
@@ -202,6 +186,19 @@ struct Overlay::Impl {
} catch (...) { cleanup(); throw; } } catch (...) { cleanup(); throw; }
} }
~Impl() { cleanup(); } ~Impl() { cleanup(); }
void update_intersection_mask() {
const auto regions = surface.input_regions();
std::vector<vr::VROverlayIntersectionMaskPrimitive_t> mask(regions.size());
for (size_t i = 0; i < mask.size(); ++i) {
const auto b = regions[i];
mask[i].m_nPrimitiveType = vr::OverlayIntersectionPrimitiveType_Rectangle;
// Mask rectangles are TOP-left pixel coordinates. Mouse events are
// bottom-left GL coordinates; flipping this mask makes the outside
// scale corner unclickable while the body happens to cover Grab.
mask[i].m_Primitive.m_Rectangle = {float(b.x), float(b.y), float(b.w), float(b.h)};
}
overlay_check(overlay->SetOverlayIntersectionMask(handle, mask.data(), uint32_t(mask.size())), overlay, "SetOverlayIntersectionMask");
}
void cleanup() { void cleanup() {
if (overlay && handle != vr::k_ulOverlayHandleInvalid) { if (overlay && handle != vr::k_ulOverlayHandleInvalid) {
overlay->HideOverlay(handle); overlay->HideOverlay(handle);
@@ -245,7 +242,8 @@ struct Overlay::Impl {
} }
if (effective == Mount::World && applied_mount && *applied_mount != Mount::World) 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 world_ready = false; // a fresh world fallback near the wearer, not an old room location
std::string note = auto_save_failed ? "Auto insert preference not saved; using it only for this session." : std::string note = layout_save_failed ? "Layout lock not saved; using it only for this session." :
auto_save_failed ? "Auto insert preference not saved; using it only for this session." :
debug_save_failed ? "Debug preference not saved; using it only for this session." : debug_save_failed ? "Debug preference not saved; using it only for this session." :
laser_change_failed ? "SteamVR declined the laser mode change." : laser_change_failed ? "SteamVR declined the laser mode change." :
save_failed ? "Preference could not be saved; using it for this session." : ""; save_failed ? "Preference could not be saved; using it for this session." : "";
@@ -267,23 +265,24 @@ 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) || size_changed) { const bool relocated = applied_mount != effective || anchor != target;
if (applied_mount != effective || anchor != target) { if (relocated || placement_dirty) {
surface.reset_pointers(); drag.reset();
move_x = move_y = 0.f;
}
const float base_width = mount_width(effective, config.wrist); const float base_width = mount_width(effective, config.wrist);
if (relocated) {
surface.reset_pointers(); drag.reset();
auto pose = effective == Mount::World ? matrix(world_transform) : relative_mount_pose(effective, config.wrist); auto pose = effective == Mount::World ? matrix(world_transform) : relative_mount_pose(effective, config.wrist);
pose = resized_mount_pose(pose, base_width, size_scale, float(CH) / CW); pose = resized_mount_pose(pose, base_width, size_scale, float(CH) / CW);
// Keep the original content width/center; transparent right/bottom // Preserve main-panel dimensions when adding transparent margins.
// margins extend the canvas, not the main panel's physical size.
const float meters_per_pixel = base_width * size_scale / CW; const float meters_per_pixel = base_width * size_scale / CW;
const float dx = move_x + (W - CW) * .5f * meters_per_pixel; const float dx = (W - CW) * .5f * meters_per_pixel;
const float dy = move_y - (H - CH) * .5f * meters_per_pixel; const float dy = -(H - CH) * .5f * meters_per_pixel;
for (int r = 0; r < 3; ++r) pose[r][3] += pose[r][0] * dx + pose[r][1] * dy; for (int r = 0; r < 3; ++r) pose[r][3] += pose[r][0] * dx + pose[r][1] * dy;
canvas_pose = pose; canvas_pose = pose;
}
// After a grab, keep the full released pose. Never rebuild it from
// a planar offset or configured orientation on the next poll.
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] = canvas_pose[r][c];
if (effective == Mount::World) if (effective == Mount::World)
overlay_check(overlay->SetOverlayTransformAbsolute(handle, vr::TrackingUniverseStanding, &transform), overlay, overlay_check(overlay->SetOverlayTransformAbsolute(handle, vr::TrackingUniverseStanding, &transform), overlay,
"SetOverlayTransformAbsolute"); "SetOverlayTransformAbsolute");
@@ -293,7 +292,7 @@ struct Overlay::Impl {
overlay_check(overlay->SetOverlayWidthInMeters(handle, base_width * size_scale * W / CW), overlay, "SetOverlayWidthInMeters"); overlay_check(overlay->SetOverlayWidthInMeters(handle, base_width * size_scale * W / CW), overlay, "SetOverlayWidthInMeters");
applied_mount = effective; applied_mount = effective;
anchor = target; anchor = target;
size_changed = false; placement_dirty = false;
} }
placed = true; placed = true;
visibility(); visibility();
@@ -306,11 +305,12 @@ struct Overlay::Impl {
auto pose = matrix(poses[drag_device].mDeviceToAbsoluteTracking); auto pose = matrix(poses[drag_device].mDeviceToAbsoluteTracking);
if (*applied_mount != Mount::World) { if (*applied_mount != Mount::World) {
if (!tracked(anchor)) return {}; if (!tracked(anchor)) return {};
pose = relative_to(pose, matrix(poses[anchor].mDeviceToAbsoluteTracking)); pose = relative_pose(matrix(poses[anchor].mDeviceToAbsoluteTracking), pose);
} }
return pose; return pose;
} }
void begin_drag(PanelDragKind kind, const vr::VREvent_t& event) { void begin_drag(PanelDragKind kind, const vr::VREvent_t& event) {
if (config.lock_layout) { surface.reset_pointers(); drag.reset(); return; }
drag_device = event.trackedDeviceIndex; drag_device = event.trackedDeviceIndex;
drag_cursor = event.data.mouse.cursorIndex; drag_cursor = event.data.mouse.cursorIndex;
// Single-cursor overlay: some runtime mouse events omit the source. // Single-cursor overlay: some runtime mouse events omit the source.
@@ -329,7 +329,7 @@ struct Overlay::Impl {
last_pointer_event = "drag source unavailable"; last_pointer_event = "drag source unavailable";
return; return;
} }
drag_scale = size_scale; drag_x = move_x; drag_y = move_y; drag_scale = size_scale; drag_canvas = canvas_pose;
drag_started = std::chrono::steady_clock::now(); drag_started = std::chrono::steady_clock::now();
vr::VRControllerState_t state{}; vr::VRControllerState_t state{};
drag_trigger_observed = system->GetControllerState(drag_device, &state, sizeof(state)) && drag_trigger_observed = system->GetControllerState(drag_device, &state, sizeof(state)) &&
@@ -354,10 +354,11 @@ struct Overlay::Impl {
const auto change = drag.update(*source); const auto change = drag.update(*source);
if (!change) { surface.reset_pointers(); drag.reset(); return; } if (!change) { surface.reset_pointers(); drag.reset(); return; }
const float scale = drag_kind == PanelDragKind::Scale ? std::clamp(drag_scale * change->factor, .5f, 2.f) : size_scale; const float scale = drag_kind == PanelDragKind::Scale ? std::clamp(drag_scale * change->factor, .5f, 2.f) : size_scale;
const float x = drag_kind == PanelDragKind::Grab ? std::clamp(drag_x + change->dx, -2.f, 2.f) : move_x; const auto pose = drag_kind == PanelDragKind::Grab ? change->pose :
const float y = drag_kind == PanelDragKind::Grab ? std::clamp(drag_y + change->dy, -2.f, 2.f) : move_y; resized_mount_pose(drag_canvas, mount_width(*applied_mount, config.wrist) * drag_scale * W / CW,
if (scale != size_scale || x != move_x || y != move_y) { scale / drag_scale, float(H) / W);
size_scale = scale; move_x = x; move_y = y; size_changed = true; if (scale != size_scale || pose != canvas_pose) {
size_scale = scale; canvas_pose = pose; placement_dirty = true;
} }
} }
bool available(UiAction action) const { return surface.available(action); } bool available(UiAction action) const { return surface.available(action); }
@@ -477,7 +478,7 @@ struct Overlay::Impl {
last_pointer_event = "up button=" + std::to_string(event.data.mouse.button); last_pointer_event = "up button=" + std::to_string(event.data.mouse.button);
if (event.data.mouse.button == vr::VRMouseButton_Left) { 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); 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.clock_24h || event_result.date_format) ++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.lock_layout || event_result.clock_24h || event_result.date_format) ++pointer_actions;
if (event_result.action) result.push_back(*event_result.action); if (event_result.action) result.push_back(*event_result.action);
if (event_result.clock_24h) { if (event_result.clock_24h) {
config.clock_24h = *event_result.clock_24h; config.clock_24h = *event_result.clock_24h;
@@ -493,6 +494,14 @@ struct Overlay::Impl {
reset_input(result); reset_input(result);
return result; return result;
} }
if (event_result.lock_layout) {
config.lock_layout = *event_result.lock_layout;
surface.set_layout_locked(config.lock_layout);
update_intersection_mask();
layout_save_failed = persist_mount && !save_lock_layout(default_config_path(), config.lock_layout);
reset_input(result); drag.reset();
return result;
}
if (event_result.auto_insert) { if (event_result.auto_insert) {
config.auto_insert = *event_result.auto_insert; config.auto_insert = *event_result.auto_insert;
auto_save_failed = persist_mount && !save_auto_insert(default_config_path(), config.auto_insert); auto_save_failed = persist_mount && !save_auto_insert(default_config_path(), config.auto_insert);
+53 -15
View File
@@ -11,7 +11,7 @@ namespace frameyap {
// mounting device); the 3x3 blocks are proper orthonormal rotations. +X is // mounting device); the 3x3 blocks are proper orthonormal rotations. +X is
// panel right, +Y panel up, and the translation is the panel center. // panel right, +Y panel up, and the translation is the panel center.
enum class PanelDragKind { Grab, Scale }; enum class PanelDragKind { Grab, Scale };
struct PanelDragUpdate { float dx, dy, factor; }; struct PanelDragUpdate { Matrix34 pose; float factor; };
namespace panel_drag_detail { namespace panel_drag_detail {
struct Vec3 { double x, y, z; }; struct Vec3 { double x, y, z; };
@@ -47,6 +47,36 @@ inline bool rigid(const Matrix34& m) {
} }
} // namespace panel_drag_detail } // namespace panel_drag_detail
// Rigid transforms only (proper orthonormal rotation, finite translation).
// compose_pose(a, b) maps a pose in a's coordinates into a's parent frame;
// relative_pose(a, b) expresses b in a's coordinates. Inputs must be valid
// rigid poses in the same frame where applicable.
inline Matrix34 compose_pose(const Matrix34& a, const Matrix34& b) {
using namespace panel_drag_detail;
Matrix34 result{};
for (int c = 0; c < 4; ++c) {
const Vec3 value = c == 3 ? rotate(a, translation(b)) + translation(a)
: rotate(a, column(b, c));
result[0][c] = static_cast<float>(value.x);
result[1][c] = static_cast<float>(value.y);
result[2][c] = static_cast<float>(value.z);
}
return result;
}
inline Matrix34 relative_pose(const Matrix34& a, const Matrix34& b) {
using namespace panel_drag_detail;
Matrix34 result{};
for (int c = 0; c < 4; ++c) {
const Vec3 value = inverse_rotate(a, c == 3 ? translation(b) - translation(a)
: column(b, c));
result[0][c] = static_cast<float>(value.x);
result[1][c] = static_cast<float>(value.y);
result[2][c] = static_cast<float>(value.z);
}
return result;
}
class PanelDrag { class PanelDrag {
public: public:
bool begin(PanelDragKind kind, const Matrix34& panel_pose, float width, float height, bool begin(PanelDragKind kind, const Matrix34& panel_pose, float width, float height,
@@ -59,6 +89,15 @@ public:
!std::isfinite(hit_x) || !std::isfinite(hit_y) || !std::isfinite(hit_x) || !std::isfinite(hit_y) ||
hit_x < 0 || hit_x > 1 || hit_y < 0 || hit_y > 1) return false; hit_x < 0 || hit_x > 1 || hit_y < 0 || hit_y > 1) return false;
if (kind == PanelDragKind::Grab) {
relative_panel_ = relative_pose(controller_pose, panel_pose);
if (!rigid(relative_panel_)) return false;
panel_pose_ = panel_pose;
kind_ = kind;
active_ = true;
return true;
}
const Vec3 center = translation(panel_pose); const Vec3 center = translation(panel_pose);
const Vec3 right = column(panel_pose, 0), up = column(panel_pose, 1); const Vec3 right = column(panel_pose, 0), up = column(panel_pose, 1);
const Vec3 normal = column(panel_pose, 2); const Vec3 normal = column(panel_pose, 2);
@@ -74,13 +113,12 @@ public:
const double baseline_sq = dot(baseline, baseline); const double baseline_sq = dot(baseline, baseline);
if (!finite(anchor) || !finite(down_hit) || !finite(direction) || if (!finite(anchor) || !finite(down_hit) || !finite(direction) ||
std::abs(dot(direction, normal)) < 1e-5 || std::abs(dot(direction, normal)) < 1e-5 ||
(kind == PanelDragKind::Scale && (!std::isfinite(baseline_sq) || baseline_sq < 1e-16))) !std::isfinite(baseline_sq) || baseline_sq < 1e-16)
return false; return false;
kind_ = kind; kind_ = kind;
panel_pose_ = panel_pose;
center_ = center; center_ = center;
right_ = right;
up_ = up;
normal_ = normal; normal_ = normal;
anchor_ = anchor; anchor_ = anchor;
down_hit_ = down_hit; down_hit_ = down_hit;
@@ -93,6 +131,11 @@ public:
std::optional<PanelDragUpdate> update(const Matrix34& controller_pose) const { std::optional<PanelDragUpdate> update(const Matrix34& controller_pose) const {
using namespace panel_drag_detail; using namespace panel_drag_detail;
if (!active_ || !rigid(controller_pose)) return std::nullopt; if (!active_ || !rigid(controller_pose)) return std::nullopt;
if (kind_ == PanelDragKind::Grab) {
const Matrix34 pose = compose_pose(controller_pose, relative_panel_);
if (!rigid(pose)) return std::nullopt;
return PanelDragUpdate{pose, 1.f};
}
const Vec3 origin = translation(controller_pose); const Vec3 origin = translation(controller_pose);
const Vec3 ray = rotate(controller_pose, local_ray_); const Vec3 ray = rotate(controller_pose, local_ray_);
const double denominator = dot(ray, normal_); const double denominator = dot(ray, normal_);
@@ -101,16 +144,10 @@ public:
const double distance = dot(center_ - origin, normal_) / denominator; const double distance = dot(center_ - origin, normal_) / denominator;
if (!std::isfinite(distance) || distance <= 0 || distance > 10) return std::nullopt; if (!std::isfinite(distance) || distance <= 0 || distance > 10) return std::nullopt;
const Vec3 hit = origin + ray * distance; const Vec3 hit = origin + ray * distance;
const Vec3 delta = hit - down_hit_; const double factor = dot(hit - anchor_, down_hit_ - anchor_) / baseline_sq_;
const double dx = kind_ == PanelDragKind::Grab ? dot(delta, right_) : 0; if (!finite(hit) || !std::isfinite(factor) ||
const double dy = kind_ == PanelDragKind::Grab ? dot(delta, up_) : 0; !std::isfinite(static_cast<float>(factor))) return std::nullopt;
const double factor = kind_ == PanelDragKind::Scale ? return PanelDragUpdate{panel_pose_, static_cast<float>(factor)};
dot(hit - anchor_, down_hit_ - anchor_) / baseline_sq_ : 1;
if (!finite(hit) || !std::isfinite(dx) || !std::isfinite(dy) ||
!std::isfinite(factor) || !std::isfinite(static_cast<float>(dx)) ||
!std::isfinite(static_cast<float>(dy)) || !std::isfinite(static_cast<float>(factor)))
return std::nullopt;
return PanelDragUpdate{static_cast<float>(dx), static_cast<float>(dy), static_cast<float>(factor)};
} }
void reset() { active_ = false; } void reset() { active_ = false; }
@@ -120,7 +157,8 @@ private:
using Vec3 = panel_drag_detail::Vec3; using Vec3 = panel_drag_detail::Vec3;
bool active_ = false; bool active_ = false;
PanelDragKind kind_ = PanelDragKind::Grab; PanelDragKind kind_ = PanelDragKind::Grab;
Vec3 center_{}, right_{}, up_{}, normal_{}, anchor_{}, down_hit_{}, local_ray_{}; Matrix34 panel_pose_{}, relative_panel_{};
Vec3 center_{}, normal_{}, anchor_{}, down_hit_{}, local_ray_{};
double baseline_sq_ = 0; double baseline_sq_ = 0;
}; };
+26 -10
View File
@@ -30,13 +30,14 @@ struct Rect {
}; };
enum class Control { Review, Settings, Bindings, Prev, Next, Record, Cancel, Insert, Enter, Quit, enum class Control { Review, Settings, Bindings, Prev, Next, Record, Cancel, Insert, Enter, Quit,
World, Left, Right, Head, Recenter, LasersAnytime, AdvancedDebug, AutoInsert, World, Left, Right, Head, Recenter, LasersAnytime, AdvancedDebug, AutoInsert,
Clock24h, Date }; Clock24h, Date, LockLayout };
enum class Tab { Review, Settings }; enum class Tab { Review, Settings };
struct Button { Rect r; Control id; const char* label; }; struct Button { Rect r; Control id; const char* label; };
constexpr std::array<Button, 20> buttons{{ constexpr std::array<Button, 21> buttons{{
{{32, 138, 180, 46}, Control::Review, "Review"}, {{32, 138, 180, 46}, Control::Review, "Review"},
{{226, 138, 180, 46}, Control::Settings, "Settings"}, {{226, 138, 180, 46}, Control::Settings, "Settings"},
{{420, 138, 180, 46}, Control::Bindings, "Bindings"}, {{420, 138, 180, 46}, Control::Bindings, "Bindings"},
{{620, 138, 348, 46}, Control::LockLayout, "Lock grab/scale"},
{{32, 406, 154, 44}, Control::Prev, "Previous"}, {{32, 406, 154, 44}, Control::Prev, "Previous"},
{{838, 406, 130, 44}, Control::Next, "Next"}, {{838, 406, 130, 44}, Control::Next, "Next"},
{{32, 574, 176, 68}, Control::Record, "Record"}, {{32, 574, 176, 68}, Control::Record, "Record"},
@@ -104,7 +105,7 @@ struct PanelSurface::Impl {
Color background, card, ink, muted, cyan, pink; Color background, card, ink, muted, cyan, pink;
Tab tab = Tab::Review; Tab tab = Tab::Review;
bool dirty = true, lasers_anytime = false, advanced_debug = false, auto_insert = false; bool dirty = true, lasers_anytime = false, advanced_debug = false, auto_insert = false;
bool clock_24h = false; bool clock_24h = false, layout_locked = false;
DateFormat date_format = DateFormat::MonthDayYear; DateFormat date_format = DateFormat::MonthDayYear;
std::time_t clock_time = std::time(nullptr); std::time_t clock_time = std::time(nullptr);
ClockLabel displayed_clock; ClockLabel displayed_clock;
@@ -254,7 +255,7 @@ struct PanelSurface::Impl {
if (c == Control::Prev || c == Control::Next) return tab == Tab::Review; if (c == Control::Prev || c == Control::Next) return tab == Tab::Review;
if (mounting(c) || c == Control::Recenter || c == Control::LasersAnytime || if (mounting(c) || c == Control::Recenter || c == Control::LasersAnytime ||
c == Control::AdvancedDebug || c == Control::AutoInsert || c == Control::AdvancedDebug || c == Control::AutoInsert ||
c == Control::Clock24h || c == Control::Date) return tab == Tab::Settings; c == Control::Clock24h || c == Control::Date || c == Control::LockLayout) return tab == Tab::Settings;
return true; return true;
} }
bool enabled(Control c) const { bool enabled(Control c) const {
@@ -330,7 +331,8 @@ struct PanelSurface::Impl {
(mounting(b.id) && *mounting(b.id) == mount) || (mounting(b.id) && *mounting(b.id) == mount) ||
(b.id == Control::LasersAnytime && lasers_anytime) || (b.id == Control::LasersAnytime && lasers_anytime) ||
(b.id == Control::AdvancedDebug && advanced_debug) || (b.id == Control::AdvancedDebug && advanced_debug) ||
(b.id == Control::AutoInsert && auto_insert); (b.id == Control::AutoInsert && auto_insert) ||
(b.id == Control::LockLayout && layout_locked);
const Color fill = !on ? mix(background, card, .40f) : const Color fill = !on ? mix(background, card, .40f) :
selected ? mix(card, cyan, .14f) : card; selected ? mix(card, cyan, .14f) : card;
const Color accent = b.id == Control::Record && panel.recording ? pink : cyan; const Color accent = b.id == Control::Record && panel.recording ? pink : cyan;
@@ -347,21 +349,23 @@ struct PanelSurface::Impl {
text(label, b.r.x + 16, b.r.y + b.r.h / 2 + 9, b.id == Control::Date ? 23 : 27, text(label, b.r.x + 16, b.r.y + b.r.h / 2 + 9, b.id == Control::Date ? 23 : 27,
on ? ink : mix(background, muted, .48f), b.r.x + b.r.w - 8); 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 (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 || b.id == Control::AdvancedDebug || b.id == Control::AutoInsert) { if (b.id == Control::LasersAnytime || b.id == Control::AdvancedDebug || b.id == Control::AutoInsert ||
b.id == Control::LockLayout) {
bool active = b.id == Control::LasersAnytime ? lasers_anytime : bool active = b.id == Control::LasersAnytime ? lasers_anytime :
b.id == Control::AutoInsert ? auto_insert : advanced_debug; b.id == Control::AutoInsert ? auto_insert :
b.id == Control::LockLayout ? layout_locked : advanced_debug;
text(active ? "ON" : "OFF", b.r.x + b.r.w - 66, b.r.y + b.r.h / 2 + 9, 22, 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); active ? cyan : muted, b.r.x + b.r.w - 12);
} }
} }
// Screenshot-inspired grab underline and outside corner bracket. No // Screenshot-inspired grab underline and outside corner bracket. No
// opaque toolbar backing; broad hit targets surround the slender strokes. // opaque toolbar backing; broad hit targets surround the slender strokes.
if (!layout_locked) {
const Color handle = theme.frame_end; const Color handle = theme.frame_end;
rounded({400, 721, 200, 6}, 3, handle, handle); rounded({400, 721, 200, 6}, 3, handle, handle);
rounded({1008, 721, 36, 6}, 3, handle, handle); rounded({1008, 721, 36, 6}, 3, handle, handle);
rounded({1038, 691, 6, 36}, 3, handle, handle); rounded({1038, 691, 6, 36}, 3, handle, handle);
// Give each handle the same full mint-to-blue theme gradient as the // The frame gradient, without changing antialiased alpha coverage.
// main frame, without changing its antialiased alpha coverage.
for (Rect bounds : {Rect{400, 721, 200, 6}, Rect{1008, 691, 36, 36}}) for (Rect bounds : {Rect{400, 721, 200, 6}, Rect{1008, 691, 36, 36}})
for (int y = bounds.y; y < bounds.y + bounds.h; ++y) for (int y = bounds.y; y < bounds.y + bounds.h; ++y)
for (int x = bounds.x; x < bounds.x + bounds.w; ++x) { for (int x = bounds.x; x < bounds.x + bounds.w; ++x) {
@@ -371,6 +375,7 @@ struct PanelSurface::Impl {
float(x - bounds.x) / (bounds.w - 1)); float(x - bounds.x) / (bounds.w - 1));
std::copy_n(color.begin(), 3, dst); std::copy_n(color.begin(), 3, dst);
} }
}
dirty = false; dirty = false;
return true; return true;
} }
@@ -387,7 +392,7 @@ bool PanelSurface::dragging(unsigned cursor) const {
std::optional<PanelDragKind> PanelSurface::pointer_down(unsigned cursor, float x, float y) { std::optional<PanelDragKind> PanelSurface::pointer_down(unsigned cursor, float x, float y) {
if (cursor >= impl_->pressed.size() || impl_->drag_cursor >= 0) return {}; if (cursor >= impl_->pressed.size() || impl_->drag_cursor >= 0) return {};
const auto contains = [&](Bounds b) { return Rect{b.x, b.y, b.w, b.h}.contains(x, y); }; const auto contains = [&](Bounds b) { return Rect{b.x, b.y, b.w, b.h}.contains(x, y); };
if (contains(grab) || contains(scale)) { if (!impl_->layout_locked && (contains(grab) || contains(scale))) {
impl_->reset(); // other cursor's prior approval cannot survive relocation impl_->reset(); // other cursor's prior approval cannot survive relocation
impl_->drag_cursor = int(cursor); impl_->drag_cursor = int(cursor);
return contains(grab) ? PanelDragKind::Grab : PanelDragKind::Scale; return contains(grab) ? PanelDragKind::Grab : PanelDragKind::Scale;
@@ -412,6 +417,7 @@ SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y) {
else if (c == Control::LasersAnytime) result.lasers_anytime = !impl_->lasers_anytime; else if (c == Control::LasersAnytime) result.lasers_anytime = !impl_->lasers_anytime;
else if (c == Control::AdvancedDebug) result.advanced_debug = !impl_->advanced_debug; else if (c == Control::AdvancedDebug) result.advanced_debug = !impl_->advanced_debug;
else if (c == Control::AutoInsert) result.auto_insert = !impl_->auto_insert; else if (c == Control::AutoInsert) result.auto_insert = !impl_->auto_insert;
else if (c == Control::LockLayout) result.lock_layout = !impl_->layout_locked;
else if (c == Control::Clock24h) result.clock_24h = !impl_->clock_24h; 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::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(); } else if (c == Control::Bindings) { result.open_bindings = true; impl_->reset(); }
@@ -427,6 +433,16 @@ void PanelSurface::set_binding_note(std::string note) {
if (impl_->binding_note != note) { impl_->binding_note = std::move(note); impl_->dirty = true; } if (impl_->binding_note != note) { impl_->binding_note = std::move(note); impl_->dirty = true; }
} }
void PanelSurface::reset_pointers() { impl_->reset(); } void PanelSurface::reset_pointers() { impl_->reset(); }
std::vector<PanelSurface::Bounds> PanelSurface::input_regions() const {
std::vector<Bounds> result{{10, 10, CW - 20, CH - 20}};
if (!impl_->layout_locked) { result.push_back(grab); result.push_back(scale); }
return result;
}
void PanelSurface::set_layout_locked(bool locked) {
if (impl_->layout_locked != locked) {
impl_->layout_locked = locked; impl_->reset(); impl_->dirty = true;
}
}
void PanelSurface::set_placement_note(std::string note) { void PanelSurface::set_placement_note(std::string note) {
if (impl_->placement_note != note) { impl_->placement_note = std::move(note); impl_->dirty = true; } if (impl_->placement_note != note) { impl_->placement_note = std::move(note); impl_->dirty = true; }
} }
+4
View File
@@ -15,6 +15,7 @@ struct SurfaceEvent {
std::optional<bool> lasers_anytime; std::optional<bool> lasers_anytime;
std::optional<bool> advanced_debug; std::optional<bool> advanced_debug;
std::optional<bool> auto_insert; std::optional<bool> auto_insert;
std::optional<bool> lock_layout;
std::optional<bool> clock_24h; std::optional<bool> clock_24h;
std::optional<DateFormat> date_format; std::optional<DateFormat> date_format;
bool recenter = false; bool recenter = false;
@@ -39,6 +40,9 @@ public:
std::optional<PanelDragKind> pointer_down(unsigned cursor, float x, float y); std::optional<PanelDragKind> 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);
bool dragging(unsigned cursor) const; 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 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);
+27
View File
@@ -42,6 +42,7 @@ int main(int argc, char** argv) {
assert(!load_config(path).experimental_input_priority); assert(!load_config(path).experimental_input_priority);
assert(!load_config(path).advanced_debug); assert(!load_config(path).advanced_debug);
assert(!load_config(path).auto_insert); assert(!load_config(path).auto_insert);
assert(!load_config(path).lock_layout);
assert(!load_config(path).clock_24h); assert(!load_config(path).clock_24h);
assert(load_config(path).date_format == DateFormat::MonthDayYear); assert(load_config(path).date_format == DateFormat::MonthDayYear);
assert(load_config(path).wrist.width == .30f); assert(load_config(path).wrist.width == .30f);
@@ -51,6 +52,7 @@ int main(int argc, char** argv) {
assert(!example.experimental_input_priority); assert(!example.experimental_input_priority);
assert(!example.advanced_debug); assert(!example.advanced_debug);
assert(!example.auto_insert); assert(!example.auto_insert);
assert(!example.lock_layout);
assert(!example.clock_24h && example.date_format == DateFormat::MonthDayYear); assert(!example.clock_24h && example.date_format == DateFormat::MonthDayYear);
assert(example.wrist.y == .18f && example.wrist.z == .089f); assert(example.wrist.y == .18f && example.wrist.z == .089f);
std::filesystem::create_directories(path.parent_path()); std::filesystem::create_directories(path.parent_path());
@@ -63,6 +65,10 @@ int main(int argc, char** argv) {
assert(load_config(path).auto_insert); assert(load_config(path).auto_insert);
assert(save_auto_insert(path, false)); assert(save_auto_insert(path, false));
assert(!load_config(path).auto_insert); assert(!load_config(path).auto_insert);
assert(save_lock_layout(path, true));
assert(load_config(path).lock_layout);
assert(save_lock_layout(path, false));
assert(!load_config(path).lock_layout);
assert(save_clock_24h(path, true) && load_config(path).clock_24h); assert(save_clock_24h(path, true) && load_config(path).clock_24h);
assert(save_date_format(path, DateFormat::Iso) && load_config(path).date_format == DateFormat::Iso); assert(save_date_format(path, DateFormat::Iso) && load_config(path).date_format == DateFormat::Iso);
assert(save_date_format(path, DateFormat::Off) && load_config(path).date_format == DateFormat::Off); assert(save_date_format(path, DateFormat::Off) && load_config(path).date_format == DateFormat::Off);
@@ -97,6 +103,7 @@ int main(int argc, char** argv) {
std::filesystem::create_symlink(path, link); std::filesystem::create_symlink(path, link);
assert(!save_advanced_debug(link, false)); assert(!save_advanced_debug(link, false));
assert(!save_auto_insert(link, true)); assert(!save_auto_insert(link, true));
assert(!save_lock_layout(link, true));
assert(get(path) == before); assert(get(path) == before);
std::filesystem::remove(link); std::filesystem::remove(link);
put(path, R"({"font":")" + std::string(4090, 'x') + R"("})"); put(path, R"({"font":")" + std::string(4090, 'x') + R"("})");
@@ -124,6 +131,26 @@ int main(int argc, char** argv) {
assert(save_auto_insert(path, true)); assert(save_auto_insert(path, true));
assert(load_config(path).auto_insert); assert(load_config(path).auto_insert);
assert(get(path) == R"({"auto_insert":true,"font":"kept"})"); assert(get(path) == R"({"auto_insert":true,"font":"kept"})");
for (const auto* invalid : {R"({"lock_layout":"true"})", R"({"lock_layout":1})",
R"({"lock_layout":null})", R"({"lock_layout":[]})",
R"({"lock_layout":{}})"}) {
put(path, invalid);
fails([&] { load_config(path); });
assert(!save_lock_layout(path, true));
assert(get(path) == invalid);
}
put(path, R"({"lock_layout":false,"font":"kept","date_format":"iso"})");
assert(save_lock_layout(path, true));
assert(load_config(path).lock_layout);
assert(get(path) == R"({"lock_layout":true,"font":"kept","date_format":"iso"})");
before = get(path);
assert(save_lock_layout(path, true) && get(path) == before);
assert(save_lock_layout(path, false));
assert(get(path) == R"({"lock_layout":false,"font":"kept","date_format":"iso"})");
put(path, R"({"font":"escaped \u0061","theme":{"ink":"#123ABC"},"auto_insert":true})");
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"({"input_priority":"normal"})"); put(path, R"({"input_priority":"normal"})");
assert(!load_config(path).experimental_input_priority); assert(!load_config(path).experimental_input_priority);
for (const auto* invalid : {R"({"input_priority":true})", R"({"input_priority":16777216})", for (const auto* invalid : {R"({"input_priority":true})", R"({"input_priority":16777216})",
+115 -58
View File
@@ -10,6 +10,11 @@ constexpr Matrix34 identity{{{{1.f, 0.f, 0.f, 0.f}},
{{0.f, 1.f, 0.f, 0.f}}, {{0.f, 1.f, 0.f, 0.f}},
{{0.f, 0.f, 1.f, 0.f}}}}; {{0.f, 0.f, 1.f, 0.f}}}};
void near(float actual, float expected) { assert(std::abs(actual - expected) < 1e-4f); } void near(float actual, float expected) { assert(std::abs(actual - expected) < 1e-4f); }
void near_pose(const Matrix34& actual, const Matrix34& expected) {
for (int row = 0; row < 3; ++row)
for (int col = 0; col < 4; ++col)
near(actual[row][col], expected[row][col]);
}
Matrix34 translated(Matrix34 pose, float x, float y, float z) { Matrix34 translated(Matrix34 pose, float x, float y, float z) {
pose[0][3] += x; pose[1][3] += y; pose[2][3] += z; pose[0][3] += x; pose[1][3] += y; pose[2][3] += z;
return pose; return pose;
@@ -17,6 +22,22 @@ Matrix34 translated(Matrix34 pose, float x, float y, float z) {
Matrix34 controller_at(float x, float y, float z = 1.f) { Matrix34 controller_at(float x, float y, float z = 1.f) {
return translated(identity, x, y, z); return translated(identity, x, y, z);
} }
// Right-handed +90 degree rotations around each world axis.
Matrix34 pitch() {
return Matrix34{{{{1.f, 0.f, 0.f, 0.f}},
{{0.f, 0.f, -1.f, 0.f}},
{{0.f, 1.f, 0.f, 0.f}}}};
}
Matrix34 yaw() {
return Matrix34{{{{0.f, 0.f, 1.f, 0.f}},
{{0.f, 1.f, 0.f, 0.f}},
{{-1.f, 0.f, 0.f, 0.f}}}};
}
Matrix34 roll() {
return Matrix34{{{{0.f, -1.f, 0.f, 0.f}},
{{1.f, 0.f, 0.f, 0.f}},
{{0.f, 0.f, 1.f, 0.f}}}};
}
void scale_case(float x, float y, float factor) { void scale_case(float x, float y, float factor) {
// 1 m square, top-left (-.5,+.5); shift controller and hence its // 1 m square, top-left (-.5,+.5); shift controller and hence its
// intersection by (factor - 1) times the original anchor-to-hit vector. // intersection by (factor - 1) times the original anchor-to-hit vector.
@@ -24,16 +45,30 @@ void scale_case(float x, float y, float factor) {
PanelDrag drag; PanelDrag drag;
assert(drag.begin(PanelDragKind::Scale, identity, 1.f, 1.f, x, y, down)); assert(drag.begin(PanelDragKind::Scale, identity, 1.f, 1.f, x, y, down));
const auto same = drag.update(down); const auto same = drag.update(down);
assert(same); near(same->factor, 1.f); near(same->dx, 0); near(same->dy, 0); assert(same); near(same->factor, 1.f); near_pose(same->pose, identity);
const auto changed = drag.update(translated(down, (factor - 1.f) * x, (1.f - factor) * y, 0)); const auto changed = drag.update(translated(down, (factor - 1.f) * x, (1.f - factor) * y, 0));
assert(changed); near(changed->factor, factor); assert(changed); near(changed->factor, factor); near_pose(changed->pose, identity);
near(changed->dx, 0); near(changed->dy, 0);
} }
// Yaw +90 degrees: panel right=-Z, up=+Y, normal=+X. void grab_rotation_case(const Matrix34& rotation, const Matrix34& expected) {
Matrix34 yawed_panel() { PanelDrag drag;
return Matrix34{{{{0.f, 0.f, 1.f, 2.f}}, // Nonzero lever arm: rotation must turn the panel position as well as its axes.
{{0.f, 1.f, 0.f, 3.f}}, const auto down = controller_at(0, 0);
{{-1.f, 0.f, 0.f, 4.f}}}}; const auto panel = translated(identity, .25f, .4f, -.3f);
assert(drag.begin(PanelDragKind::Grab, panel, 1, 1, .7f, .3f, down));
auto turned = rotation;
turned[2][3] = 1.f;
const auto update = drag.update(turned);
assert(update); near(update->factor, 1.f); near_pose(update->pose, expected);
// No accumulation: moving back to down restores the exact initial pose.
drag.reset();
assert(!drag.update(turned));
// Release while turned, then pick that rotated panel up again elsewhere.
const auto second_down = translated(turned, .12f, -.08f, .2f);
assert(drag.begin(PanelDragKind::Grab, update->pose, 1, 1, .4f, .6f, second_down));
const auto restored = drag.update(second_down);
assert(restored); near_pose(restored->pose, update->pose);
const auto shifted = drag.update(translated(second_down, 0.f, 0.f, -.4f));
assert(shifted); near_pose(shifted->pose, translated(update->pose, 0.f, 0.f, -.4f));
} }
} // namespace } // namespace
@@ -41,16 +76,37 @@ int main() {
PanelDrag drag; PanelDrag drag;
assert(!drag.active()); assert(!drag.update(controller_at(0, 0))); assert(!drag.active()); assert(!drag.update(controller_at(0, 0)));
const auto down = controller_at(0, 0); const auto down = controller_at(0, 0);
assert(drag.begin(PanelDragKind::Grab, identity, 1.f, 1.f, .5f, .5f, down)); auto panel = translated(identity, .25f, .4f, -.3f);
assert(drag.begin(PanelDragKind::Grab, panel, 1.f, 1.f, .5f, .5f, down));
assert(drag.active()); assert(drag.active());
for (int i = 0; i < 10; ++i) {
const auto still = drag.update(down); const auto still = drag.update(down);
assert(still); near(still->dx, 0); near(still->dy, 0); near(still->factor, 1); assert(still); near_pose(still->pose, panel); near(still->factor, 1.f);
auto moved = drag.update(controller_at(.23f, -.16f)); }
assert(moved); near(moved->dx, .23f); near(moved->dy, -.16f); near(moved->factor, 1); auto moved = drag.update(controller_at(.23f, -.16f, 1.45f));
// The original plane is fixed: the ray may hit outside the initial canvas. assert(moved); near_pose(moved->pose, translated(panel, .23f, -.16f, .45f));
moved = drag.update(controller_at(2.f, 1.f)); const auto released_pose = moved->pose;
assert(moved); near(moved->dx, 2.f); near(moved->dy, 1.f);
drag.reset(); assert(!drag.active()); assert(!drag.update(down)); drag.reset(); assert(!drag.active()); assert(!drag.update(down));
// A subsequent grab begins at the dropped position and orientation, not the
// old calibration; releasing leaves the caller's last pose unchanged.
const auto rotated_pose = compose_pose(translated(yaw(), 0.f, .1f, 0.f), released_pose);
const auto second_down = controller_at(-.2f, .1f, .9f);
assert(drag.begin(PanelDragKind::Grab, rotated_pose, 1, 1, .5f, .5f, second_down));
moved = drag.update(second_down);
assert(moved); near_pose(moved->pose, rotated_pose);
moved = drag.update(translated(second_down, -.1f, .2f, -.3f));
assert(moved); near_pose(moved->pose, translated(rotated_pose, -.1f, .2f, -.3f));
drag.reset(); assert(!drag.update(second_down));
grab_rotation_case(pitch(), Matrix34{{{{1.f, 0.f, 0.f, .25f}},
{{0.f, 0.f, -1.f, 1.3f}},
{{0.f, 1.f, 0.f, 1.4f}}}});
grab_rotation_case(yaw(), Matrix34{{{{0.f, 0.f, 1.f, -1.3f}},
{{0.f, 1.f, 0.f, .4f}},
{{-1.f, 0.f, 0.f, .75f}}}});
grab_rotation_case(roll(), Matrix34{{{{0.f, -1.f, 0.f, -.4f}},
{{1.f, 0.f, 0.f, .25f}},
{{0.f, 0.f, 1.f, -.3f}}}});
// Independent horizontal, vertical, and diagonal anchor-based scaling; // Independent horizontal, vertical, and diagonal anchor-based scaling;
// both contraction and growth, including intersections outside the panel. // both contraction and growth, including intersections outside the panel.
@@ -59,14 +115,12 @@ int main() {
scale_case(0.f, .5f, factor); scale_case(0.f, .5f, factor);
scale_case(.5f, .5f, factor); scale_case(.5f, .5f, factor);
} }
assert(drag.begin(PanelDragKind::Scale, identity, 1, 1, .5f, .5f, assert(drag.begin(PanelDragKind::Scale, identity, 1, 1, .5f, .5f, down));
controller_at(0, 0))); moved = drag.update(controller_at(.25f, .25f)); // perpendicular to anchor vector
// A perpendicular offset does not alter projected scale.
moved = drag.update(controller_at(.25f, .25f));
assert(moved); near(moved->factor, 1); assert(moved); near(moved->factor, 1);
moved = drag.update(controller_at(.5f, -.5f)); moved = drag.update(controller_at(.5f, -.5f));
assert(moved); near(moved->factor, 2); assert(moved); near(moved->factor, 2);
moved = drag.update(controller_at(0, 0)); // no feedback from the previous size moved = drag.update(down); // no feedback from previous size
assert(moved); near(moved->factor, 1); assert(moved); near(moved->factor, 1);
drag.reset(); drag.reset();
assert(!drag.begin(PanelDragKind::Scale, identity, 1, 1, 0, 0, assert(!drag.begin(PanelDragKind::Scale, identity, 1, 1, 0, 0,
@@ -74,49 +128,40 @@ int main() {
assert(drag.begin(PanelDragKind::Grab, identity, 1, 1, 0, 0, assert(drag.begin(PanelDragKind::Grab, identity, 1, 1, 0, 0,
controller_at(-.5f, .5f))); controller_at(-.5f, .5f)));
// Rotating the controller changes the calibrated ray even without moving // Scaling still uses the captured controller-local ray, not the grab pose.
// its origin. The initial ray is (.25,0,-1), rotated 90 degrees about Z.
auto angled_down = controller_at(0, 0); auto angled_down = controller_at(0, 0);
assert(drag.begin(PanelDragKind::Grab, identity, 1, 1, .75f, .5f, angled_down)); assert(drag.begin(PanelDragKind::Scale, identity, 1, 1, .75f, .5f, angled_down));
auto turned = angled_down; auto turned = angled_down;
turned[0][0] = 0; turned[0][1] = -1; turned[0][0] = 0; turned[0][1] = -1;
turned[1][0] = 1; turned[1][1] = 0; turned[1][0] = 1; turned[1][1] = 0;
moved = drag.update(turned); moved = drag.update(turned);
assert(moved); near(moved->dx, -.25f); near(moved->dy, .25f); assert(moved); near(moved->factor, .5f / .8125f); near_pose(moved->pose, identity);
const auto panel = yawed_panel(); const auto yawed_panel = translated(yaw(), 2.f, 3.f, 4.f);
// Use the same rotated basis for both controller and panel; a 0.2 m shift auto corner_ray = translated(yawed_panel, 1.f, .25f, .25f);
// along panel right (-Z) yields panel-right grab displacement. assert(drag.begin(PanelDragKind::Scale, yawed_panel, 1, 1, .25f, .25f, corner_ray));
auto rotated_down = panel;
rotated_down[0][3] += 1.f;
assert(drag.begin(PanelDragKind::Grab, panel, 1, 1, .5f, .5f, rotated_down));
moved = drag.update(translated(rotated_down, 0, -.15f, -.2f));
assert(moved); near(moved->dx, .2f); near(moved->dy, -.15f);
auto corner_ray = rotated_down;
corner_ray[1][3] += .25f;
corner_ray[2][3] += .25f; // initial hit at x=.25, y=.25
assert(drag.begin(PanelDragKind::Scale, panel, 1, 1, .25f, .25f, corner_ray));
moved = drag.update(translated(corner_ray, 0, -.25f, -.25f)); moved = drag.update(translated(corner_ray, 0, -.25f, -.25f));
assert(moved); near(moved->factor, 2.f); assert(moved); near(moved->factor, 2.f); near_pose(moved->pose, yawed_panel);
// Relative poses are valid without converting them to world space: both // The identical grab in a device-relative frame and under an arbitrary
// inputs here share a rotated/translated parent coordinate system. // rigid parent in world space must produce equivalent world panel poses.
auto relative_panel = identity; const auto parent = translated(compose_pose(yaw(), pitch()), 2.f, 3.f, 4.f);
relative_panel[0][3] = -.3f; relative_panel[1][3] = .1f; const auto relative_panel = translated(roll(), -.3f, .1f, -.1f);
auto relative_controller = translated(relative_panel, 0, 0, .8f); const auto relative_controller = controller_at(.1f, -.2f, .8f);
const auto relative_next = compose_pose(translated(yaw(), .08f, -.04f, .2f), relative_controller);
assert(drag.begin(PanelDragKind::Grab, relative_panel, .4f, .2f, assert(drag.begin(PanelDragKind::Grab, relative_panel, .4f, .2f,
.5f, .5f, relative_controller)); .5f, .5f, relative_controller));
moved = drag.update(translated(relative_controller, .08f, -.04f, 0)); moved = drag.update(relative_next);
assert(moved); near(moved->dx, .08f); near(moved->dy, -.04f); assert(moved);
// Express the same poses in world coordinates under a yawed parent: const auto relative_result = moved->pose;
// (x, y, z) -> (2+z, 3+y, 4-x). The update is invariant. const auto world_panel = compose_pose(parent, relative_panel);
auto world_panel = yawed_panel(); const auto world_controller = compose_pose(parent, relative_controller);
world_panel[1][3] += .1f; world_panel[2][3] += .3f;
auto world_controller = translated(world_panel, .8f, 0, 0);
assert(drag.begin(PanelDragKind::Grab, world_panel, .4f, .2f, assert(drag.begin(PanelDragKind::Grab, world_panel, .4f, .2f,
.5f, .5f, world_controller)); .5f, .5f, world_controller));
moved = drag.update(translated(world_controller, 0, -.04f, -.08f)); moved = drag.update(compose_pose(parent, relative_next));
assert(moved); near(moved->dx, .08f); near(moved->dy, -.04f); assert(moved); near_pose(moved->pose, compose_pose(parent, relative_result));
near_pose(relative_pose(parent, moved->pose), relative_result);
near_pose(compose_pose(world_controller, relative_pose(world_controller, world_panel)), world_panel);
const float nan = std::numeric_limits<float>::quiet_NaN(); const float nan = std::numeric_limits<float>::quiet_NaN();
const float inf = std::numeric_limits<float>::infinity(); const float inf = std::numeric_limits<float>::infinity();
@@ -133,24 +178,36 @@ int main() {
assert(!drag.begin(PanelDragKind::Grab, identity, inf, 1, .5f, .5f, down)); assert(!drag.begin(PanelDragKind::Grab, identity, inf, 1, .5f, .5f, down));
assert(!drag.begin(PanelDragKind::Grab, identity, 1, 1, nan, .5f, down)); assert(!drag.begin(PanelDragKind::Grab, identity, 1, 1, nan, .5f, down));
assert(!drag.begin(PanelDragKind::Grab, identity, 1, 1, -1, .5f, down)); assert(!drag.begin(PanelDragKind::Grab, identity, 1, 1, -1, .5f, down));
assert(!drag.begin(PanelDragKind::Grab, identity, 1, 1, .5f, .5f, identity)); // zero ray // Both inputs are finite but their relative translation cannot fit in Matrix34.
assert(!drag.begin(PanelDragKind::Grab, identity, 1, 1, .5f, .5f, const float max = std::numeric_limits<float>::max();
assert(!drag.begin(PanelDragKind::Grab, translated(identity, -max, 0, 0),
1, 1, .5f, .5f, translated(identity, max, 0, 0)));
assert(!drag.active()); assert(!drag.update(down));
assert(drag.begin(PanelDragKind::Grab, identity, 1, 1, .5f, .5f, identity));
assert(drag.update(identity)); // grab does not require a ray to the panel
drag.reset();
assert(!drag.begin(PanelDragKind::Scale, identity, 1, 1, .5f, .5f, identity)); // zero ray
assert(!drag.begin(PanelDragKind::Scale, identity, 1, 1, .5f, .5f,
controller_at(1.f, 0.f, 1e-7f))); // near parallel at down controller_at(1.f, 0.f, 1e-7f))); // near parallel at down
assert(!drag.begin(PanelDragKind::Grab, identity, 1, 1, .5f, .5f, assert(!drag.begin(PanelDragKind::Scale, identity, 1, 1, .5f, .5f,
controller_at(0, 0, 11.f))); // beyond 10 m controller_at(0, 0, 11.f))); // beyond 10 m
assert(drag.begin(PanelDragKind::Grab, identity, 1, 1, .5f, .5f, down)); assert(drag.begin(PanelDragKind::Scale, identity, 1, 1, .5f, .5f, down));
bad = down; bad[1][2] = inf; bad = down; bad[1][2] = inf;
assert(!drag.update(bad)); assert(!drag.update(bad));
bad = down; bad[0][0] = 0; bad = down; bad[0][0] = 0;
assert(!drag.update(bad)); assert(!drag.update(bad));
assert(!drag.update(controller_at(0, 0, -1.f))); // behind ray source assert(!drag.update(controller_at(0, 0, -1.f))); // behind ray source
assert(!drag.update(controller_at(0, 0, 11.f))); // beyond 10 m assert(!drag.update(controller_at(0, 0, 11.f))); // beyond 10 m
// Rotate -Z to -X: ray parallel to initial panel plane.
bad = down; bad = down;
bad[0][0] = 0; bad[0][2] = 1; bad[0][0] = 0; bad[0][2] = 1;
bad[2][0] = -1; bad[2][2] = 0; bad[2][0] = -1; bad[2][2] = 0;
assert(!drag.update(bad)); assert(!drag.update(bad)); // ray parallel to initial panel plane
assert(drag.active()); // invalid update leaves original calibration intact assert(drag.active()); // invalid update leaves original calibration intact
assert(drag.update(down)); assert(drag.update(down));
drag.reset(); assert(!drag.active()); drag.reset(); assert(!drag.active()); assert(!drag.update(down));
assert(drag.begin(PanelDragKind::Grab, identity, 1, 1, .5f, .5f, down));
bad = down; bad[2][3] = nan;
assert(!drag.update(bad));
assert(drag.update(down));
drag.reset(); assert(!drag.update(down));
} }
+24 -1
View File
@@ -14,7 +14,7 @@ SurfaceEvent click(PanelSurface& surface, float x, float y, unsigned cursor = 0)
} }
void no_action(const SurfaceEvent& event) { void no_action(const SurfaceEvent& event) {
assert(!event.action && !event.mount && !event.lasers_anytime && !event.advanced_debug && !event.auto_insert && assert(!event.action && !event.mount && !event.lasers_anytime && !event.advanced_debug && !event.auto_insert &&
!event.clock_24h && !event.date_format && !event.recenter && !event.open_bindings); !event.lock_layout && !event.clock_24h && !event.date_format && !event.recenter && !event.open_bindings);
} }
void snapshot(PanelSurface& surface, const std::string& path) { void snapshot(PanelSurface& surface, const std::string& path) {
std::ofstream out(path, std::ios::binary); std::ofstream out(path, std::ios::binary);
@@ -109,6 +109,21 @@ int main(int argc, char** argv) {
assert(channel(1012, 723, 0) > 220 && channel(1040, 705, 2) > 220); assert(channel(1012, 723, 0) > 220 && channel(1040, 705, 2) > 220);
assert(channel(1030, 704, 3) == 0 && channel(800, 724, 3) == 0); assert(channel(1030, 704, 3) == 0 && channel(800, 724, 3) == 0);
assert(channel(401, 721, 3) == alpha(401, 721)); assert(channel(401, 721, 3) == alpha(401, 721));
auto regions = surface.input_regions();
assert(regions.size() == 3);
assert(regions[2].x == 996 && regions[2].y == 680); // masks use TOP-left, not GL mouse Y
surface.pointer_down(0, 500, 724);
surface.set_layout_locked(true);
assert(!surface.dragging(0));
assert(surface.render(p));
assert(alpha(500, 723) == 0 && alpha(1040, 705) == 0);
assert(surface.input_regions().size() == 1);
assert(!surface.pointer_down(0, 500, 724));
assert(!surface.pointer_down(0, 1028, 712));
no_action(surface.pointer_up(0, 900, 610));
assert(!surface.render(p));
surface.set_layout_locked(false); assert(surface.render(p));
assert(alpha(500, 723) == 255 && alpha(1040, 705) == 255);
p.enabled = true; p.enabled = true;
p.record_available = false; p.record_available = false;
@@ -148,6 +163,13 @@ int main(int argc, char** argv) {
surface.render(p); surface.render(p);
surface.reset_pointers(); surface.render(p); surface.reset_pointers(); surface.render(p);
if (argc >= 3) snapshot(surface, std::string(argv[2]) + "-settings.ppm"); if (argc >= 3) snapshot(surface, std::string(argv[2]) + "-settings.ppm");
auto lock = click(surface, 790, 160);
assert(lock.lock_layout == true && !lock.action);
assert(!surface.render(p)); // caller applies/persists the requested lock
surface.set_layout_locked(true); assert(surface.render(p));
lock = click(surface, 790, 160);
assert(lock.lock_layout == false); // unlock stays accessible in Settings
surface.set_layout_locked(false); assert(surface.render(p));
assert(click(surface, 200, 420).recenter); assert(click(surface, 200, 420).recenter);
auto event = click(surface, 180, 332); auto event = click(surface, 180, 332);
assert(event.mount == Mount::LeftWrist && !event.action); assert(event.mount == Mount::LeftWrist && !event.action);
@@ -206,6 +228,7 @@ int main(int argc, char** argv) {
no_action(click(surface, 680, 420)); // laser toggle only exists on Settings no_action(click(surface, 680, 420)); // laser toggle only exists on Settings
no_action(click(surface, 680, 474)); // debug toggle only exists on Settings no_action(click(surface, 680, 474)); // debug toggle only exists on Settings
no_action(click(surface, 200, 474)); // auto insert only exists on Settings no_action(click(surface, 200, 474)); // auto insert only exists on Settings
no_action(click(surface, 790, 160)); // layout lock only exists on Settings
no_action(click(surface, 200, 528)); // clock/date controls only exist on Settings no_action(click(surface, 200, 528)); // clock/date controls only exist on Settings
no_action(click(surface, 680, 528)); no_action(click(surface, 680, 528));
+18
View File
@@ -75,6 +75,7 @@ class InstallTests(unittest.TestCase):
self.assertEqual(json.loads(config.read_text()), installer.CONFIG_DEFAULTS) self.assertEqual(json.loads(config.read_text()), installer.CONFIG_DEFAULTS)
self.assertIs(json.loads(config.read_text())["advanced_debug"], False) self.assertIs(json.loads(config.read_text())["advanced_debug"], False)
self.assertIs(json.loads(config.read_text())["auto_insert"], False) self.assertIs(json.loads(config.read_text())["auto_insert"], False)
self.assertIs(json.loads(config.read_text())["lock_layout"], False)
self.assertEqual(list(config.parent.glob("config.json.backup-*")), []) self.assertEqual(list(config.parent.glob("config.json.backup-*")), [])
self.assertIn("PYTHONDONTWRITEBYTECODE=1", launcher.read_text()) self.assertIn("PYTHONDONTWRITEBYTECODE=1", launcher.read_text())
self.assertTrue(os.access(root / "versions/v1/runtime/bin/helper", os.X_OK)) self.assertTrue(os.access(root / "versions/v1/runtime/bin/helper", os.X_OK))
@@ -129,6 +130,7 @@ class InstallTests(unittest.TestCase):
self.assertEqual(fixed["input_priority"], "normal") self.assertEqual(fixed["input_priority"], "normal")
self.assertIs(fixed["advanced_debug"], False) self.assertIs(fixed["advanced_debug"], False)
self.assertIs(fixed["auto_insert"], False) self.assertIs(fixed["auto_insert"], False)
self.assertIs(fixed["lock_layout"], False)
self.assertIs(fixed["clock_24h"], False) self.assertIs(fixed["clock_24h"], False)
self.assertEqual(fixed["date_format"], "mdy") self.assertEqual(fixed["date_format"], "mdy")
self.assertEqual(fixed["wrist"], installer.CONFIG_DEFAULTS["wrist"]) self.assertEqual(fixed["wrist"], installer.CONFIG_DEFAULTS["wrist"])
@@ -138,6 +140,7 @@ class InstallTests(unittest.TestCase):
fixed["input_priority"] = "experimental" fixed["input_priority"] = "experimental"
fixed["advanced_debug"] = True fixed["advanced_debug"] = True
fixed["auto_insert"] = True fixed["auto_insert"] = True
fixed["lock_layout"] = True
fixed["clock_24h"] = True fixed["clock_24h"] = True
fixed["date_format"] = "iso" fixed["date_format"] = "iso"
fixed["buttons"]["enter"] = "" # intentional disabling survives upgrades fixed["buttons"]["enter"] = "" # intentional disabling survives upgrades
@@ -148,6 +151,7 @@ class InstallTests(unittest.TestCase):
self.assertEqual(config.read_bytes(), compact) self.assertEqual(config.read_bytes(), compact)
self.assertIs(json.loads(config.read_text())["advanced_debug"], True) self.assertIs(json.loads(config.read_text())["advanced_debug"], True)
self.assertIs(json.loads(config.read_text())["auto_insert"], True) self.assertIs(json.loads(config.read_text())["auto_insert"], True)
self.assertIs(json.loads(config.read_text())["lock_layout"], True)
self.assertIs(json.loads(config.read_text())["clock_24h"], True) self.assertIs(json.loads(config.read_text())["clock_24h"], True)
self.assertEqual(json.loads(config.read_text())["date_format"], "iso") self.assertEqual(json.loads(config.read_text())["date_format"], "iso")
self.assertEqual(len(list(config.parent.glob("config.json.backup-*"))), 1) self.assertEqual(len(list(config.parent.glob("config.json.backup-*"))), 1)
@@ -167,6 +171,7 @@ class InstallTests(unittest.TestCase):
self.assertEqual(fixed["input_priority"], "normal") self.assertEqual(fixed["input_priority"], "normal")
self.assertIs(fixed["advanced_debug"], False) self.assertIs(fixed["advanced_debug"], False)
self.assertIs(fixed["auto_insert"], False) self.assertIs(fixed["auto_insert"], False)
self.assertIs(fixed["lock_layout"], False)
self.assertEqual(fixed["wrist"]["x"], 0.04) self.assertEqual(fixed["wrist"]["x"], 0.04)
self.assertEqual(fixed["wrist"]["y"], 0.18) self.assertEqual(fixed["wrist"]["y"], 0.18)
self.assertEqual(fixed["wrist"]["width"], 0.30) self.assertEqual(fixed["wrist"]["width"], 0.30)
@@ -215,6 +220,19 @@ class InstallTests(unittest.TestCase):
self.assertIs(json.loads(config.read_text())["auto_insert"], False) self.assertIs(json.loads(config.read_text())["auto_insert"], False)
self.assertIn(original, [p.read_bytes() for p in config.parent.glob("config.json.backup-*")]) self.assertIn(original, [p.read_bytes() for p in config.parent.glob("config.json.backup-*")])
def test_lock_layout_boolean_repair_backs_up_invalid_values(self):
archive, digest = self.package("v1")
config = self.home / ".config/frameyap/config.json"
config.parent.mkdir(parents=True)
for invalid in ("true", 1, None, [], {}):
original = json.dumps({"lock_layout": invalid, "font": "/custom/font.ttf"}).encode()
config.write_bytes(original)
self.install("v1", archive, digest)
fixed = json.loads(config.read_text())
self.assertIs(fixed["lock_layout"], False)
self.assertEqual(fixed["font"], "/custom/font.ttf")
self.assertIn(original, [p.read_bytes() for p in config.parent.glob("config.json.backup-*")])
def test_digest_and_same_version_mismatch_leave_previous(self): def test_digest_and_same_version_mismatch_leave_previous(self):
a, h = self.package("v1") a, h = self.package("v1")
self.install("v1", a, h) self.install("v1", a, h)