mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 00:00:05 +02:00
Add rigid controller grab and layout lock; fix scale hit mask
This commit is contained in:
1 parent
9c7fdae264
commit
0d4cfbfdb7
15 files changed
+387
-162
No files matched your search
@@ -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.
|
||||
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.
|
||||
Drag the thin bar below the panel to move it in its plane; drag the external
|
||||
lower-right corner bracket to scale it (world, head or wrist). Both sit in
|
||||
transparent margins, like Steam's window handles. Scaling keeps the upper-left
|
||||
corner anchored. Position/size changes last for this run only.
|
||||
Hold the thin bar below the panel to freely move and rotate it with your
|
||||
controller; release to leave it at that pose. Drag the external lower-right
|
||||
bracket to scale it (world, head or wrist), keeping the upper-left anchored.
|
||||
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
|
||||
upright viewer-facing pose (60°–75°), hiding interaction past that angle;
|
||||
world and head mounting do not fade.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"input_priority": "normal",
|
||||
"advanced_debug": false,
|
||||
"auto_insert": false,
|
||||
"lock_layout": false,
|
||||
"clock_24h": false,
|
||||
"date_format": "mdy",
|
||||
"wrist": {"x": 0, "y": 0.18, "z": 0.089, "width": 0.30, "roll_degrees": 0},
|
||||
|
||||
+30
-14
@@ -105,6 +105,7 @@ installer creates one with defaults on first install. Copy the shipped
|
||||
"input_priority": "normal",
|
||||
"advanced_debug": false,
|
||||
"auto_insert": false,
|
||||
"lock_layout": false,
|
||||
"clock_24h": false,
|
||||
"date_format": "mdy",
|
||||
"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
|
||||
leaves their surrounding area transparent; an explicit OpenVR intersection mask
|
||||
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
|
||||
plane**; this does not change depth or orientation. Drag the corner bracket to scale
|
||||
between half and twice the configured width. The original upper-left corner remains
|
||||
fixed relative to the chosen mount. Both work on either tab and all mounts. Movement
|
||||
is bounded to two meters per axis; mount changes/recenter clear movement, while mount
|
||||
changes retain the size factor. Restart restores configured position/size defaults.
|
||||
Hold the laser's primary click on the bar to **freely position and rotate** the
|
||||
panel with the controller, including depth, pitch, yaw and roll. Grab captures
|
||||
`inverse(controller_down) * panel_down` and applies that unchanged relative pose
|
||||
to each controller pose, so grabbing does not snap or reset the panel orientation.
|
||||
Release leaves the last pose in the chosen mount frame; head/wrist mounts continue
|
||||
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
|
||||
the initial hit. Subsequent tracked poses determine translation/scale, even beyond
|
||||
the original texture bounds; changing overlay coordinates never feed back into the
|
||||
drag. For head/wrist mounts, geometry stays in the anchor-device coordinate frame.
|
||||
Drag the corner bracket to scale between half and twice the configured width.
|
||||
Scaling freezes its initial plane and calibrates a ray from the source controller
|
||||
to the initial hit. The top-left corner of the current, potentially rotated panel
|
||||
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
|
||||
fallback when the event omits it. No guessed controller or desktop pointer fallback.
|
||||
Release, changed UI authorization, tracking loss, hidden overlay, relocation, invalid
|
||||
@@ -304,9 +318,11 @@ coexistence remains unverified.
|
||||
### Hardware-free UI checks
|
||||
|
||||
The default build tests mount parsing, laser preference persistence, pose geometry
|
||||
and captured-ray grab/scale math without any native dependencies. Drag tests cover
|
||||
stationary stability, independent axes, rotated/relative mounts, out-of-bounds hits,
|
||||
invalid rays and no feedback from prior updates. A FreeType-only opt-in build exercises the actual renderer,
|
||||
and controller-relative grab/captured-ray scale math without any native dependencies.
|
||||
Drag tests cover XYZ translation, pitch/yaw/roll and lever-arm rotation, re-grabbing
|
||||
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:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -36,6 +36,7 @@ CONFIG_DEFAULTS = {
|
||||
"input_priority": "normal",
|
||||
"advanced_debug": False,
|
||||
"auto_insert": False,
|
||||
"lock_layout": False,
|
||||
"clock_24h": False,
|
||||
"date_format": "mdy",
|
||||
"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
|
||||
automatic = data.get("auto_insert", 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)
|
||||
fixed["clock_24h"] = clock if type(clock) is bool else False
|
||||
date = data.get("date_format", "mdy")
|
||||
|
||||
@@ -25,6 +25,7 @@ CONFIG_DEFAULTS = {
|
||||
"input_priority": "normal",
|
||||
"advanced_debug": False,
|
||||
"auto_insert": False,
|
||||
"lock_layout": False,
|
||||
"clock_24h": False,
|
||||
"date_format": "mdy",
|
||||
"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
|
||||
automatic = data.get("auto_insert", 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)
|
||||
fixed["clock_24h"] = clock if type(clock) is bool else False
|
||||
date = data.get("date_format", "mdy")
|
||||
|
||||
@@ -200,6 +200,9 @@ Config load_config(const std::filesystem::path& path) {
|
||||
} else if (key == "auto_insert") {
|
||||
if (!value.is_bool) throw std::runtime_error("Config auto_insert must be a boolean");
|
||||
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") {
|
||||
if (!value.is_bool) throw std::runtime_error("Config clock_24h must be a boolean");
|
||||
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 {
|
||||
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 {
|
||||
return save_bool_option(path, "clock_24h", enabled);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ struct Config {
|
||||
bool experimental_input_priority = false;
|
||||
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 lock_layout = false; // hide the grab and scale handles when enabled
|
||||
bool clock_24h = false;
|
||||
DateFormat date_format = DateFormat::MonthDayYear;
|
||||
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.
|
||||
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_lock_layout(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;
|
||||
std::string resolve_font(const std::string& assets, const std::string& requested);
|
||||
|
||||
+55
-46
@@ -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];
|
||||
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"}};
|
||||
void overlay_check(vr::EVROverlayError err, vr::IVROverlay* api, const char* op) {
|
||||
if (err != vr::VROverlayError_None)
|
||||
@@ -90,19 +83,18 @@ struct Overlay::Impl {
|
||||
Config config;
|
||||
PanelSurface surface;
|
||||
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;
|
||||
float published_alpha = -1.f;
|
||||
float size_scale = 1.f;
|
||||
bool size_changed = false;
|
||||
float move_x = 0.f, move_y = 0.f;
|
||||
Matrix34 canvas_pose{};
|
||||
bool placement_dirty = false;
|
||||
Matrix34 canvas_pose{}, drag_canvas{};
|
||||
PanelDrag drag;
|
||||
PanelDragKind drag_kind = PanelDragKind::Grab;
|
||||
unsigned drag_cursor = 0;
|
||||
vr::TrackedDeviceIndex_t drag_device = vr::k_unTrackedDeviceIndexInvalid;
|
||||
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{};
|
||||
vr::HmdMatrix34_t world_transform{};
|
||||
std::optional<Mount> applied_mount;
|
||||
@@ -180,21 +172,13 @@ struct Overlay::Impl {
|
||||
surface.set_lasers_anytime(lasers_anytime);
|
||||
surface.set_advanced_debug(config.advanced_debug);
|
||||
surface.set_auto_insert(config.auto_insert);
|
||||
surface.set_layout_locked(config.lock_layout);
|
||||
surface.set_clock_24h(config.clock_24h);
|
||||
surface.set_date_format(config.date_format);
|
||||
overlay_check(overlay->SetOverlayFlag(handle, vr::VROverlayFlags_VisibleInDashboard, true), overlay, "VisibleInDashboard");
|
||||
vr::HmdVector2_t mouse_scale{{float(W), float(H)}};
|
||||
overlay_check(overlay->SetOverlayMouseScale(handle, &mouse_scale), overlay, "SetOverlayMouseScale");
|
||||
// Alpha is visual, not an input mask. Exclude the empty margins from
|
||||
// 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");
|
||||
update_intersection_mask();
|
||||
system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, poses.data(), uint32_t(poses.size()));
|
||||
place();
|
||||
draw(Panel{"Disabled", "", "Record to start local worker", false, false});
|
||||
@@ -202,6 +186,19 @@ struct Overlay::Impl {
|
||||
} catch (...) { cleanup(); throw; }
|
||||
}
|
||||
~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() {
|
||||
if (overlay && handle != vr::k_ulOverlayHandleInvalid) {
|
||||
overlay->HideOverlay(handle);
|
||||
@@ -245,7 +242,8 @@ struct Overlay::Impl {
|
||||
}
|
||||
if (effective == Mount::World && applied_mount && *applied_mount != Mount::World)
|
||||
world_ready = false; // a fresh world fallback near the wearer, not an old room location
|
||||
std::string note = 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." :
|
||||
laser_change_failed ? "SteamVR declined the laser mode change." :
|
||||
save_failed ? "Preference could not be saved; using it for this session." : "";
|
||||
@@ -267,23 +265,24 @@ struct Overlay::Impl {
|
||||
applied_mount.reset();
|
||||
}
|
||||
surface.set_placement_note(note);
|
||||
if (applied_mount != effective || (effective != Mount::World && anchor != target) || size_changed) {
|
||||
if (applied_mount != effective || anchor != target) {
|
||||
surface.reset_pointers(); drag.reset();
|
||||
move_x = move_y = 0.f;
|
||||
}
|
||||
const bool relocated = applied_mount != effective || anchor != target;
|
||||
if (relocated || placement_dirty) {
|
||||
const float base_width = mount_width(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);
|
||||
// Keep the original content width/center; transparent right/bottom
|
||||
// margins extend the canvas, not the main panel's physical size.
|
||||
const float meters_per_pixel = base_width * size_scale / CW;
|
||||
const float dx = move_x + (W - CW) * .5f * meters_per_pixel;
|
||||
const float dy = move_y - (H - CH) * .5f * meters_per_pixel;
|
||||
for (int r = 0; r < 3; ++r) pose[r][3] += pose[r][0] * dx + pose[r][1] * dy;
|
||||
canvas_pose = pose;
|
||||
if (relocated) {
|
||||
surface.reset_pointers(); drag.reset();
|
||||
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);
|
||||
// Preserve main-panel dimensions when adding transparent margins.
|
||||
const float meters_per_pixel = base_width * size_scale / CW;
|
||||
const float dx = (W - CW) * .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;
|
||||
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{};
|
||||
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)
|
||||
overlay_check(overlay->SetOverlayTransformAbsolute(handle, vr::TrackingUniverseStanding, &transform), overlay,
|
||||
"SetOverlayTransformAbsolute");
|
||||
@@ -293,7 +292,7 @@ struct Overlay::Impl {
|
||||
overlay_check(overlay->SetOverlayWidthInMeters(handle, base_width * size_scale * W / CW), overlay, "SetOverlayWidthInMeters");
|
||||
applied_mount = effective;
|
||||
anchor = target;
|
||||
size_changed = false;
|
||||
placement_dirty = false;
|
||||
}
|
||||
placed = true;
|
||||
visibility();
|
||||
@@ -306,11 +305,12 @@ struct Overlay::Impl {
|
||||
auto pose = matrix(poses[drag_device].mDeviceToAbsoluteTracking);
|
||||
if (*applied_mount != Mount::World) {
|
||||
if (!tracked(anchor)) return {};
|
||||
pose = relative_to(pose, matrix(poses[anchor].mDeviceToAbsoluteTracking));
|
||||
pose = relative_pose(matrix(poses[anchor].mDeviceToAbsoluteTracking), pose);
|
||||
}
|
||||
return pose;
|
||||
}
|
||||
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_cursor = event.data.mouse.cursorIndex;
|
||||
// Single-cursor overlay: some runtime mouse events omit the source.
|
||||
@@ -329,7 +329,7 @@ struct Overlay::Impl {
|
||||
last_pointer_event = "drag source unavailable";
|
||||
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();
|
||||
vr::VRControllerState_t state{};
|
||||
drag_trigger_observed = system->GetControllerState(drag_device, &state, sizeof(state)) &&
|
||||
@@ -354,10 +354,11 @@ struct Overlay::Impl {
|
||||
const auto change = drag.update(*source);
|
||||
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 x = drag_kind == PanelDragKind::Grab ? std::clamp(drag_x + change->dx, -2.f, 2.f) : move_x;
|
||||
const float y = drag_kind == PanelDragKind::Grab ? std::clamp(drag_y + change->dy, -2.f, 2.f) : move_y;
|
||||
if (scale != size_scale || x != move_x || y != move_y) {
|
||||
size_scale = scale; move_x = x; move_y = y; size_changed = true;
|
||||
const auto pose = drag_kind == PanelDragKind::Grab ? change->pose :
|
||||
resized_mount_pose(drag_canvas, mount_width(*applied_mount, config.wrist) * drag_scale * W / CW,
|
||||
scale / drag_scale, float(H) / W);
|
||||
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); }
|
||||
@@ -477,7 +478,7 @@ struct Overlay::Impl {
|
||||
last_pointer_event = "up button=" + std::to_string(event.data.mouse.button);
|
||||
if (event.data.mouse.button == vr::VRMouseButton_Left) {
|
||||
auto event_result = surface.pointer_up(event.data.mouse.cursorIndex, event.data.mouse.x, H - event.data.mouse.y);
|
||||
if (event_result.action || event_result.mount || event_result.recenter || 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.clock_24h) {
|
||||
config.clock_24h = *event_result.clock_24h;
|
||||
@@ -493,6 +494,14 @@ struct Overlay::Impl {
|
||||
reset_input(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) {
|
||||
config.auto_insert = *event_result.auto_insert;
|
||||
auto_save_failed = persist_mount && !save_auto_insert(default_config_path(), config.auto_insert);
|
||||
|
||||
+53
-15
@@ -11,7 +11,7 @@ namespace frameyap {
|
||||
// mounting device); the 3x3 blocks are proper orthonormal rotations. +X is
|
||||
// panel right, +Y panel up, and the translation is the panel center.
|
||||
enum class PanelDragKind { Grab, Scale };
|
||||
struct PanelDragUpdate { float dx, dy, factor; };
|
||||
struct PanelDragUpdate { Matrix34 pose; float factor; };
|
||||
|
||||
namespace panel_drag_detail {
|
||||
struct Vec3 { double x, y, z; };
|
||||
@@ -47,6 +47,36 @@ inline bool rigid(const Matrix34& m) {
|
||||
}
|
||||
} // 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 {
|
||||
public:
|
||||
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) ||
|
||||
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 right = column(panel_pose, 0), up = column(panel_pose, 1);
|
||||
const Vec3 normal = column(panel_pose, 2);
|
||||
@@ -74,13 +113,12 @@ public:
|
||||
const double baseline_sq = dot(baseline, baseline);
|
||||
if (!finite(anchor) || !finite(down_hit) || !finite(direction) ||
|
||||
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;
|
||||
|
||||
kind_ = kind;
|
||||
panel_pose_ = panel_pose;
|
||||
center_ = center;
|
||||
right_ = right;
|
||||
up_ = up;
|
||||
normal_ = normal;
|
||||
anchor_ = anchor;
|
||||
down_hit_ = down_hit;
|
||||
@@ -93,6 +131,11 @@ public:
|
||||
std::optional<PanelDragUpdate> update(const Matrix34& controller_pose) const {
|
||||
using namespace panel_drag_detail;
|
||||
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 ray = rotate(controller_pose, local_ray_);
|
||||
const double denominator = dot(ray, normal_);
|
||||
@@ -101,16 +144,10 @@ public:
|
||||
const double distance = dot(center_ - origin, normal_) / denominator;
|
||||
if (!std::isfinite(distance) || distance <= 0 || distance > 10) return std::nullopt;
|
||||
const Vec3 hit = origin + ray * distance;
|
||||
const Vec3 delta = hit - down_hit_;
|
||||
const double dx = kind_ == PanelDragKind::Grab ? dot(delta, right_) : 0;
|
||||
const double dy = kind_ == PanelDragKind::Grab ? dot(delta, up_) : 0;
|
||||
const double factor = kind_ == PanelDragKind::Scale ?
|
||||
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)};
|
||||
const double factor = dot(hit - anchor_, down_hit_ - anchor_) / baseline_sq_;
|
||||
if (!finite(hit) || !std::isfinite(factor) ||
|
||||
!std::isfinite(static_cast<float>(factor))) return std::nullopt;
|
||||
return PanelDragUpdate{panel_pose_, static_cast<float>(factor)};
|
||||
}
|
||||
|
||||
void reset() { active_ = false; }
|
||||
@@ -120,7 +157,8 @@ private:
|
||||
using Vec3 = panel_drag_detail::Vec3;
|
||||
bool active_ = false;
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
+39
-23
@@ -30,13 +30,14 @@ 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 };
|
||||
Clock24h, Date, LockLayout };
|
||||
enum class Tab { Review, Settings };
|
||||
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"},
|
||||
{{226, 138, 180, 46}, Control::Settings, "Settings"},
|
||||
{{420, 138, 180, 46}, Control::Bindings, "Bindings"},
|
||||
{{620, 138, 348, 46}, Control::LockLayout, "Lock grab/scale"},
|
||||
{{32, 406, 154, 44}, Control::Prev, "Previous"},
|
||||
{{838, 406, 130, 44}, Control::Next, "Next"},
|
||||
{{32, 574, 176, 68}, Control::Record, "Record"},
|
||||
@@ -104,7 +105,7 @@ 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 clock_24h = false;
|
||||
bool clock_24h = false, layout_locked = false;
|
||||
DateFormat date_format = DateFormat::MonthDayYear;
|
||||
std::time_t clock_time = std::time(nullptr);
|
||||
ClockLabel displayed_clock;
|
||||
@@ -254,7 +255,7 @@ struct PanelSurface::Impl {
|
||||
if (c == Control::Prev || c == Control::Next) return tab == Tab::Review;
|
||||
if (mounting(c) || c == Control::Recenter || c == Control::LasersAnytime ||
|
||||
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;
|
||||
}
|
||||
bool enabled(Control c) const {
|
||||
@@ -330,7 +331,8 @@ struct PanelSurface::Impl {
|
||||
(mounting(b.id) && *mounting(b.id) == mount) ||
|
||||
(b.id == Control::LasersAnytime && lasers_anytime) ||
|
||||
(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) :
|
||||
selected ? mix(card, cyan, .14f) : card;
|
||||
const Color accent = b.id == Control::Record && panel.recording ? pink : cyan;
|
||||
@@ -347,30 +349,33 @@ struct PanelSurface::Impl {
|
||||
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);
|
||||
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 :
|
||||
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,
|
||||
active ? cyan : muted, b.r.x + b.r.w - 12);
|
||||
}
|
||||
}
|
||||
// Screenshot-inspired grab underline and outside corner bracket. No
|
||||
// opaque toolbar backing; broad hit targets surround the slender strokes.
|
||||
const Color handle = theme.frame_end;
|
||||
rounded({400, 721, 200, 6}, 3, handle, handle);
|
||||
rounded({1008, 721, 36, 6}, 3, handle, handle);
|
||||
rounded({1038, 691, 6, 36}, 3, handle, handle);
|
||||
// Give each handle the same full mint-to-blue theme gradient as the
|
||||
// main frame, without changing its antialiased alpha coverage.
|
||||
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 x = bounds.x; x < bounds.x + bounds.w; ++x) {
|
||||
auto* dst = pixels.data() + (size_t(y) * W + x) * 4;
|
||||
if (!dst[3]) continue;
|
||||
const auto color = mix(theme.frame_start, theme.frame_end,
|
||||
float(x - bounds.x) / (bounds.w - 1));
|
||||
std::copy_n(color.begin(), 3, dst);
|
||||
}
|
||||
if (!layout_locked) {
|
||||
const Color handle = theme.frame_end;
|
||||
rounded({400, 721, 200, 6}, 3, handle, handle);
|
||||
rounded({1008, 721, 36, 6}, 3, handle, handle);
|
||||
rounded({1038, 691, 6, 36}, 3, handle, handle);
|
||||
// The frame gradient, without changing antialiased alpha coverage.
|
||||
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 x = bounds.x; x < bounds.x + bounds.w; ++x) {
|
||||
auto* dst = pixels.data() + (size_t(y) * W + x) * 4;
|
||||
if (!dst[3]) continue;
|
||||
const auto color = mix(theme.frame_start, theme.frame_end,
|
||||
float(x - bounds.x) / (bounds.w - 1));
|
||||
std::copy_n(color.begin(), 3, dst);
|
||||
}
|
||||
}
|
||||
dirty = false;
|
||||
return true;
|
||||
}
|
||||
@@ -387,7 +392,7 @@ bool PanelSurface::dragging(unsigned cursor) const {
|
||||
std::optional<PanelDragKind> PanelSurface::pointer_down(unsigned cursor, float x, float y) {
|
||||
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); };
|
||||
if (contains(grab) || contains(scale)) {
|
||||
if (!impl_->layout_locked && (contains(grab) || contains(scale))) {
|
||||
impl_->reset(); // other cursor's prior approval cannot survive relocation
|
||||
impl_->drag_cursor = int(cursor);
|
||||
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::AdvancedDebug) result.advanced_debug = !impl_->advanced_debug;
|
||||
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::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(); }
|
||||
@@ -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; }
|
||||
}
|
||||
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) {
|
||||
if (impl_->placement_note != note) { impl_->placement_note = std::move(note); impl_->dirty = true; }
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ struct SurfaceEvent {
|
||||
std::optional<bool> lasers_anytime;
|
||||
std::optional<bool> advanced_debug;
|
||||
std::optional<bool> auto_insert;
|
||||
std::optional<bool> lock_layout;
|
||||
std::optional<bool> clock_24h;
|
||||
std::optional<DateFormat> date_format;
|
||||
bool recenter = false;
|
||||
@@ -39,6 +40,9 @@ public:
|
||||
std::optional<PanelDragKind> pointer_down(unsigned cursor, float x, float y);
|
||||
SurfaceEvent pointer_up(unsigned cursor, float x, float y);
|
||||
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 set_placement_note(std::string note);
|
||||
void set_lasers_anytime(bool enabled);
|
||||
|
||||
@@ -42,6 +42,7 @@ int main(int argc, char** argv) {
|
||||
assert(!load_config(path).experimental_input_priority);
|
||||
assert(!load_config(path).advanced_debug);
|
||||
assert(!load_config(path).auto_insert);
|
||||
assert(!load_config(path).lock_layout);
|
||||
assert(!load_config(path).clock_24h);
|
||||
assert(load_config(path).date_format == DateFormat::MonthDayYear);
|
||||
assert(load_config(path).wrist.width == .30f);
|
||||
@@ -51,6 +52,7 @@ int main(int argc, char** argv) {
|
||||
assert(!example.experimental_input_priority);
|
||||
assert(!example.advanced_debug);
|
||||
assert(!example.auto_insert);
|
||||
assert(!example.lock_layout);
|
||||
assert(!example.clock_24h && example.date_format == DateFormat::MonthDayYear);
|
||||
assert(example.wrist.y == .18f && example.wrist.z == .089f);
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
@@ -63,6 +65,10 @@ int main(int argc, char** argv) {
|
||||
assert(load_config(path).auto_insert);
|
||||
assert(save_auto_insert(path, false));
|
||||
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_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);
|
||||
@@ -97,6 +103,7 @@ int main(int argc, char** argv) {
|
||||
std::filesystem::create_symlink(path, link);
|
||||
assert(!save_advanced_debug(link, false));
|
||||
assert(!save_auto_insert(link, true));
|
||||
assert(!save_lock_layout(link, true));
|
||||
assert(get(path) == before);
|
||||
std::filesystem::remove(link);
|
||||
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(load_config(path).auto_insert);
|
||||
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"})");
|
||||
assert(!load_config(path).experimental_input_priority);
|
||||
for (const auto* invalid : {R"({"input_priority":true})", R"({"input_priority":16777216})",
|
||||
|
||||
+116
-59
@@ -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, 0.f, 1.f, 0.f}}}};
|
||||
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) {
|
||||
pose[0][3] += x; pose[1][3] += y; pose[2][3] += z;
|
||||
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) {
|
||||
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) {
|
||||
// 1 m square, top-left (-.5,+.5); shift controller and hence its
|
||||
// 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;
|
||||
assert(drag.begin(PanelDragKind::Scale, identity, 1.f, 1.f, x, y, 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));
|
||||
assert(changed); near(changed->factor, factor);
|
||||
near(changed->dx, 0); near(changed->dy, 0);
|
||||
assert(changed); near(changed->factor, factor); near_pose(changed->pose, identity);
|
||||
}
|
||||
// Yaw +90 degrees: panel right=-Z, up=+Y, normal=+X.
|
||||
Matrix34 yawed_panel() {
|
||||
return Matrix34{{{{0.f, 0.f, 1.f, 2.f}},
|
||||
{{0.f, 1.f, 0.f, 3.f}},
|
||||
{{-1.f, 0.f, 0.f, 4.f}}}};
|
||||
void grab_rotation_case(const Matrix34& rotation, const Matrix34& expected) {
|
||||
PanelDrag drag;
|
||||
// Nonzero lever arm: rotation must turn the panel position as well as its axes.
|
||||
const auto down = controller_at(0, 0);
|
||||
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
|
||||
|
||||
@@ -41,16 +76,37 @@ int main() {
|
||||
PanelDrag drag;
|
||||
assert(!drag.active()); assert(!drag.update(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());
|
||||
const auto still = drag.update(down);
|
||||
assert(still); near(still->dx, 0); near(still->dy, 0); near(still->factor, 1);
|
||||
auto moved = drag.update(controller_at(.23f, -.16f));
|
||||
assert(moved); near(moved->dx, .23f); near(moved->dy, -.16f); near(moved->factor, 1);
|
||||
// The original plane is fixed: the ray may hit outside the initial canvas.
|
||||
moved = drag.update(controller_at(2.f, 1.f));
|
||||
assert(moved); near(moved->dx, 2.f); near(moved->dy, 1.f);
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
const auto still = drag.update(down);
|
||||
assert(still); near_pose(still->pose, panel); near(still->factor, 1.f);
|
||||
}
|
||||
auto moved = drag.update(controller_at(.23f, -.16f, 1.45f));
|
||||
assert(moved); near_pose(moved->pose, translated(panel, .23f, -.16f, .45f));
|
||||
const auto released_pose = moved->pose;
|
||||
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;
|
||||
// both contraction and growth, including intersections outside the panel.
|
||||
@@ -59,14 +115,12 @@ int main() {
|
||||
scale_case(0.f, .5f, factor);
|
||||
scale_case(.5f, .5f, factor);
|
||||
}
|
||||
assert(drag.begin(PanelDragKind::Scale, identity, 1, 1, .5f, .5f,
|
||||
controller_at(0, 0)));
|
||||
// A perpendicular offset does not alter projected scale.
|
||||
moved = drag.update(controller_at(.25f, .25f));
|
||||
assert(drag.begin(PanelDragKind::Scale, identity, 1, 1, .5f, .5f, down));
|
||||
moved = drag.update(controller_at(.25f, .25f)); // perpendicular to anchor vector
|
||||
assert(moved); near(moved->factor, 1);
|
||||
moved = drag.update(controller_at(.5f, -.5f));
|
||||
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);
|
||||
drag.reset();
|
||||
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,
|
||||
controller_at(-.5f, .5f)));
|
||||
|
||||
// Rotating the controller changes the calibrated ray even without moving
|
||||
// its origin. The initial ray is (.25,0,-1), rotated 90 degrees about Z.
|
||||
// Scaling still uses the captured controller-local ray, not the grab pose.
|
||||
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;
|
||||
turned[0][0] = 0; turned[0][1] = -1;
|
||||
turned[1][0] = 1; turned[1][1] = 0;
|
||||
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();
|
||||
// Use the same rotated basis for both controller and panel; a 0.2 m shift
|
||||
// along panel right (-Z) yields panel-right grab displacement.
|
||||
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));
|
||||
const auto yawed_panel = translated(yaw(), 2.f, 3.f, 4.f);
|
||||
auto corner_ray = translated(yawed_panel, 1.f, .25f, .25f);
|
||||
assert(drag.begin(PanelDragKind::Scale, yawed_panel, 1, 1, .25f, .25f, corner_ray));
|
||||
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
|
||||
// inputs here share a rotated/translated parent coordinate system.
|
||||
auto relative_panel = identity;
|
||||
relative_panel[0][3] = -.3f; relative_panel[1][3] = .1f;
|
||||
auto relative_controller = translated(relative_panel, 0, 0, .8f);
|
||||
// The identical grab in a device-relative frame and under an arbitrary
|
||||
// rigid parent in world space must produce equivalent world panel poses.
|
||||
const auto parent = translated(compose_pose(yaw(), pitch()), 2.f, 3.f, 4.f);
|
||||
const auto relative_panel = translated(roll(), -.3f, .1f, -.1f);
|
||||
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,
|
||||
.5f, .5f, relative_controller));
|
||||
moved = drag.update(translated(relative_controller, .08f, -.04f, 0));
|
||||
assert(moved); near(moved->dx, .08f); near(moved->dy, -.04f);
|
||||
// Express the same poses in world coordinates under a yawed parent:
|
||||
// (x, y, z) -> (2+z, 3+y, 4-x). The update is invariant.
|
||||
auto world_panel = yawed_panel();
|
||||
world_panel[1][3] += .1f; world_panel[2][3] += .3f;
|
||||
auto world_controller = translated(world_panel, .8f, 0, 0);
|
||||
moved = drag.update(relative_next);
|
||||
assert(moved);
|
||||
const auto relative_result = moved->pose;
|
||||
const auto world_panel = compose_pose(parent, relative_panel);
|
||||
const auto world_controller = compose_pose(parent, relative_controller);
|
||||
assert(drag.begin(PanelDragKind::Grab, world_panel, .4f, .2f,
|
||||
.5f, .5f, world_controller));
|
||||
moved = drag.update(translated(world_controller, 0, -.04f, -.08f));
|
||||
assert(moved); near(moved->dx, .08f); near(moved->dy, -.04f);
|
||||
moved = drag.update(compose_pose(parent, relative_next));
|
||||
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 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, 1, 1, nan, .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
|
||||
assert(!drag.begin(PanelDragKind::Grab, identity, 1, 1, .5f, .5f,
|
||||
// Both inputs are finite but their relative translation cannot fit in Matrix34.
|
||||
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
|
||||
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
|
||||
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;
|
||||
assert(!drag.update(bad));
|
||||
bad = down; bad[0][0] = 0;
|
||||
assert(!drag.update(bad));
|
||||
assert(!drag.update(controller_at(0, 0, -1.f))); // behind ray source
|
||||
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[0][0] = 0; bad[0][2] = 1;
|
||||
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.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
@@ -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.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) {
|
||||
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(1030, 704, 3) == 0 && channel(800, 724, 3) == 0);
|
||||
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.record_available = false;
|
||||
@@ -148,6 +163,13 @@ int main(int argc, char** argv) {
|
||||
surface.render(p);
|
||||
surface.reset_pointers(); surface.render(p);
|
||||
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);
|
||||
auto event = click(surface, 180, 332);
|
||||
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, 474)); // debug toggle 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, 680, 528));
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ class InstallTests(unittest.TestCase):
|
||||
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())["auto_insert"], False)
|
||||
self.assertIs(json.loads(config.read_text())["lock_layout"], False)
|
||||
self.assertEqual(list(config.parent.glob("config.json.backup-*")), [])
|
||||
self.assertIn("PYTHONDONTWRITEBYTECODE=1", launcher.read_text())
|
||||
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.assertIs(fixed["advanced_debug"], False)
|
||||
self.assertIs(fixed["auto_insert"], False)
|
||||
self.assertIs(fixed["lock_layout"], False)
|
||||
self.assertIs(fixed["clock_24h"], False)
|
||||
self.assertEqual(fixed["date_format"], "mdy")
|
||||
self.assertEqual(fixed["wrist"], installer.CONFIG_DEFAULTS["wrist"])
|
||||
@@ -138,6 +140,7 @@ class InstallTests(unittest.TestCase):
|
||||
fixed["input_priority"] = "experimental"
|
||||
fixed["advanced_debug"] = True
|
||||
fixed["auto_insert"] = True
|
||||
fixed["lock_layout"] = True
|
||||
fixed["clock_24h"] = True
|
||||
fixed["date_format"] = "iso"
|
||||
fixed["buttons"]["enter"] = "" # intentional disabling survives upgrades
|
||||
@@ -148,6 +151,7 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertEqual(config.read_bytes(), compact)
|
||||
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())["lock_layout"], True)
|
||||
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)
|
||||
@@ -167,6 +171,7 @@ class InstallTests(unittest.TestCase):
|
||||
self.assertEqual(fixed["input_priority"], "normal")
|
||||
self.assertIs(fixed["advanced_debug"], False)
|
||||
self.assertIs(fixed["auto_insert"], False)
|
||||
self.assertIs(fixed["lock_layout"], False)
|
||||
self.assertEqual(fixed["wrist"]["x"], 0.04)
|
||||
self.assertEqual(fixed["wrist"]["y"], 0.18)
|
||||
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.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):
|
||||
a, h = self.package("v1")
|
||||
self.install("v1", a, h)
|
||||
|
||||
Reference in new issue
Block a user