mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 01:00:04 +02:00
Fix grab and scale with transparent external handles
This commit is contained in:
1 parent
468f35622e
commit
33533ba8a2
9 files changed
+527
-86
No files matched your search
@@ -131,6 +131,10 @@ if(BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING)
|
||||
target_include_directories(frameyap_lock_test PRIVATE src)
|
||||
add_test(NAME frameyap.instance_lock COMMAND frameyap_lock_test)
|
||||
endif()
|
||||
add_executable(frameyap_panel_drag_test tests/panel_drag_test.cpp)
|
||||
target_include_directories(frameyap_panel_drag_test PRIVATE src)
|
||||
target_compile_options(frameyap_panel_drag_test PRIVATE -UNDEBUG)
|
||||
add_test(NAME frameyap.panel_drag COMMAND frameyap_panel_drag_test)
|
||||
add_executable(frameyap_gestures_test tests/gestures_test.cpp)
|
||||
target_include_directories(frameyap_gestures_test PRIVATE src)
|
||||
target_compile_options(frameyap_gestures_test PRIVATE -UNDEBUG)
|
||||
|
||||
@@ -29,8 +29,10 @@ See [third-party notes](docs/third-party.md). No GitHub release is published yet
|
||||
Nothing submits automatically. Auto Insert is opt-in and off by default.
|
||||
- **Overlay:** Record/Stop, Cancel, paginated preview, Insert, Enter and Quit.
|
||||
Review and settings share one Inconsolata/neon-framed surface.
|
||||
Drag the inset lower-right grip to resize the panel (world, head or wrist);
|
||||
the upper-left corner stays anchored. Size changes last for this run only.
|
||||
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.
|
||||
**Bindings** requests SteamVR's binding editor directly. Existing SteamVR overrides may supersede defaults. World-space by
|
||||
default; settings offer left wrist, right wrist and head mounting, plus recenter.
|
||||
Dashboard lasers provide clickable controls. Settings → Lasers anytime is an
|
||||
|
||||
+34
-10
@@ -18,8 +18,9 @@ A missing selected font falls back to bundled Inconsolata, then a system DejaVu
|
||||
Sans face if present. Glyph coverage depends on the selected face; full CJK
|
||||
coverage is not claimed.
|
||||
|
||||
`src/panel_surface.*` renders **one 1000×680 RGBA canvas** for review, settings,
|
||||
status and controls. `src/overlay_texture.*` uploads this CPU canvas into one
|
||||
`src/panel_surface.*` renders **one 1080×780 RGBA canvas** containing a 1000×680
|
||||
main panel for review, settings, status and controls, plus transparent right/bottom
|
||||
margins for a thin grab underline and an external L-shaped scale handle. `src/overlay_texture.*` uploads this CPU canvas into one
|
||||
persistent Vulkan RGBA8 image and submits it with `SetOverlayTexture`. The image,
|
||||
staging allocation and command buffer are reused; tabs do not create extra
|
||||
overlays or render targets. The rounded mint-to-blue perimeter,
|
||||
@@ -203,12 +204,33 @@ a fresh placement. Settings → Recenter in front deliberately resamples the pos
|
||||
|
||||
Settings offers World space, Left wrist, Right wrist and Head on that same canvas.
|
||||
World/head width is 0.85 m; wrist width defaults to 0.30 m. All mounts have
|
||||
the same lower-right resize grip: drag it to scale the panel
|
||||
between half and twice its configured width. The original upper-left corner
|
||||
remains fixed relative to the chosen mount, rather than scaling about the
|
||||
center. The physical resize is session-only; switching mounts retains the
|
||||
scale factor, while restart restores configured defaults. Grip behavior and
|
||||
comfort still need an opt-in on-headset check. The left wrist uses
|
||||
a thin grab bar below the main panel and an L-shaped scale bracket outside its
|
||||
lower-right corner, visually modeled on the user's Steam terminal-window screenshot.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
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
|
||||
ray geometry or a 15-second safety limit cancels the drag. If legacy trigger state is
|
||||
observable on press, release is also checked through that state, including outside
|
||||
the texture. Otherwise the drag cancels as soon as this overlay stops being the
|
||||
hover target, rather than waiting for a potentially missing outside MouseButtonUp. Other pointer approvals and bound actions cannot fire during a drag.
|
||||
Source-device reporting, out-of-bounds release, mask behavior and comfort still need
|
||||
on-headset acceptance. The left wrist uses
|
||||
VR Workspace's fallback watch-face axes: panel-right points toward the fingers
|
||||
(controller -Z), panel-up points out of the back of the hand (controller +Y),
|
||||
and panel-front points toward controller +X. The right wrist reverses panel-right
|
||||
@@ -258,8 +280,10 @@ coexistence remains unverified.
|
||||
|
||||
### Hardware-free UI checks
|
||||
|
||||
The default build tests mount parsing, laser preference persistence and pose
|
||||
geometry without any native dependencies. A FreeType-only opt-in build exercises the actual renderer,
|
||||
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,
|
||||
pointer gating, tab switches, pagination, recording state and redraw invalidation:
|
||||
|
||||
```sh
|
||||
|
||||
+126
-21
@@ -34,6 +34,19 @@ void configure_registry() {
|
||||
::setenv("VR_PATHREG_OVERRIDE", registry.c_str(), 0);
|
||||
}
|
||||
constexpr int W = PanelSurface::width, H = PanelSurface::height;
|
||||
constexpr int CW = PanelSurface::body.w, CH = PanelSurface::body.h;
|
||||
Matrix34 matrix(const vr::HmdMatrix34_t& value) {
|
||||
Matrix34 result{};
|
||||
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)
|
||||
@@ -78,6 +91,15 @@ struct Overlay::Impl {
|
||||
bool world_ready = false, placed = false, has_texture = false, shown = false;
|
||||
float size_scale = 1.f;
|
||||
bool size_changed = false;
|
||||
float move_x = 0.f, move_y = 0.f;
|
||||
Matrix34 canvas_pose{};
|
||||
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;
|
||||
std::chrono::steady_clock::time_point drag_started{};
|
||||
vr::HmdMatrix34_t world_transform{};
|
||||
std::optional<Mount> applied_mount;
|
||||
vr::TrackedDeviceIndex_t anchor = vr::k_unTrackedDeviceIndexInvalid;
|
||||
@@ -157,6 +179,16 @@ struct Overlay::Impl {
|
||||
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");
|
||||
system->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, poses.data(), uint32_t(poses.size()));
|
||||
place();
|
||||
draw(Panel{"Disabled", "", "Record to start local worker", false, false});
|
||||
@@ -216,26 +248,29 @@ struct Overlay::Impl {
|
||||
}
|
||||
surface.set_placement_note(note);
|
||||
if (applied_mount != effective || (effective != Mount::World && anchor != target) || size_changed) {
|
||||
if (!size_changed || applied_mount != effective || anchor != target)
|
||||
surface.reset_pointers(); // a release on a relocated surface cannot activate an old press
|
||||
if (applied_mount != effective || anchor != target) {
|
||||
surface.reset_pointers(); drag.reset();
|
||||
move_x = move_y = 0.f;
|
||||
}
|
||||
const float base_width = mount_width(effective, config.wrist);
|
||||
if (effective == Mount::World) {
|
||||
Matrix34 pose{};
|
||||
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) pose[r][c] = world_transform.m[r][c];
|
||||
pose = resized_mount_pose(pose, base_width, size_scale, float(H) / W);
|
||||
vr::HmdMatrix34_t transform{};
|
||||
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) transform.m[r][c] = pose[r][c];
|
||||
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;
|
||||
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];
|
||||
if (effective == Mount::World)
|
||||
overlay_check(overlay->SetOverlayTransformAbsolute(handle, vr::TrackingUniverseStanding, &transform), overlay,
|
||||
"SetOverlayTransformAbsolute");
|
||||
} else {
|
||||
const auto pose = resized_mount_pose(relative_mount_pose(effective, config.wrist),
|
||||
base_width, size_scale, float(H) / W);
|
||||
vr::HmdMatrix34_t transform{};
|
||||
for (int r = 0; r < 3; ++r) for (int c = 0; c < 4; ++c) transform.m[r][c] = pose[r][c];
|
||||
else
|
||||
overlay_check(overlay->SetOverlayTransformTrackedDeviceRelative(handle, target, &transform), overlay,
|
||||
"SetOverlayTransformTrackedDeviceRelative");
|
||||
}
|
||||
overlay_check(overlay->SetOverlayWidthInMeters(handle, base_width * size_scale), overlay, "SetOverlayWidthInMeters");
|
||||
overlay_check(overlay->SetOverlayWidthInMeters(handle, base_width * size_scale * W / CW), overlay, "SetOverlayWidthInMeters");
|
||||
applied_mount = effective;
|
||||
anchor = target;
|
||||
size_changed = false;
|
||||
@@ -243,6 +278,68 @@ struct Overlay::Impl {
|
||||
placed = true;
|
||||
visibility();
|
||||
}
|
||||
bool tracked(vr::TrackedDeviceIndex_t device) const {
|
||||
return device < poses.size() && poses[device].bPoseIsValid && poses[device].bDeviceIsConnected;
|
||||
}
|
||||
std::optional<Matrix34> drag_source() const {
|
||||
if (!tracked(drag_device) || !applied_mount) return {};
|
||||
auto pose = matrix(poses[drag_device].mDeviceToAbsoluteTracking);
|
||||
if (*applied_mount != Mount::World) {
|
||||
if (!tracked(anchor)) return {};
|
||||
pose = relative_to(pose, matrix(poses[anchor].mDeviceToAbsoluteTracking));
|
||||
}
|
||||
return pose;
|
||||
}
|
||||
void begin_drag(PanelDragKind kind, const vr::VREvent_t& event) {
|
||||
drag_device = event.trackedDeviceIndex;
|
||||
drag_cursor = event.data.mouse.cursorIndex;
|
||||
// Single-cursor overlay: some runtime mouse events omit the source.
|
||||
// The dashboard's primary device is the only supported fallback, never
|
||||
// a guessed left/right hand or a source chosen by proximity.
|
||||
if (drag_cursor == 0 && (drag_device == vr::k_unTrackedDeviceIndexInvalid ||
|
||||
drag_device == vr::k_unTrackedDeviceIndex_Hmd))
|
||||
drag_device = overlay->GetPrimaryDashboardDevice();
|
||||
drag_kind = kind;
|
||||
const auto source = drag_source();
|
||||
const float w = applied_mount ? mount_width(*applied_mount, config.wrist) * size_scale * W / CW : 0.f;
|
||||
if (!source || system->GetTrackedDeviceClass(drag_device) != vr::TrackedDeviceClass_Controller ||
|
||||
!drag.begin(kind, canvas_pose, w, w * H / W, event.data.mouse.x / W,
|
||||
1.f - event.data.mouse.y / H, *source)) {
|
||||
surface.reset_pointers(); drag.reset();
|
||||
last_pointer_event = "drag source unavailable";
|
||||
return;
|
||||
}
|
||||
drag_scale = size_scale; drag_x = move_x; drag_y = move_y;
|
||||
drag_started = std::chrono::steady_clock::now();
|
||||
vr::VRControllerState_t state{};
|
||||
drag_trigger_observed = system->GetControllerState(drag_device, &state, sizeof(state)) &&
|
||||
(state.ulButtonPressed & vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger));
|
||||
last_pointer_event = std::string(kind == PanelDragKind::Grab ? "grab" : "scale") +
|
||||
" device=" + std::to_string(drag_device) + " trigger-watch=" + (drag_trigger_observed ? "Y" : "N");
|
||||
}
|
||||
void update_drag() {
|
||||
if (!drag.active()) return;
|
||||
const auto source = drag_source();
|
||||
vr::VRControllerState_t state{};
|
||||
const bool trigger_released = drag_trigger_observed &&
|
||||
(!system->GetControllerState(drag_device, &state, sizeof(state)) ||
|
||||
!(state.ulButtonPressed & vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger)));
|
||||
// Without a readable release watchdog, never keep manipulating after
|
||||
// the pointer leaves our hit region: an outside MouseUp is not assured.
|
||||
const bool lost_unwatched_pointer = !drag_trigger_observed && !overlay->IsHoverTargetOverlay(handle);
|
||||
if (!surface.dragging(drag_cursor) || !shown || !focus || !source || trigger_released || lost_unwatched_pointer ||
|
||||
std::chrono::steady_clock::now() - drag_started > std::chrono::seconds(15)) {
|
||||
surface.reset_pointers(); drag.reset(); return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
bool available(UiAction action) const { return surface.available(action); }
|
||||
void draw(const Panel& p) {
|
||||
panel = p;
|
||||
@@ -344,17 +441,15 @@ struct Overlay::Impl {
|
||||
// a release must still hit the same enabled control.
|
||||
last_pointer_event = "overlay focus changed"; break;
|
||||
case vr::VREvent_MouseMove:
|
||||
if (auto factor = surface.pointer_move(event.data.mouse.cursorIndex, event.data.mouse.x,
|
||||
H - event.data.mouse.y)) {
|
||||
const float next = std::clamp(size_scale * *factor, .5f, 2.f);
|
||||
if (next != size_scale) { size_scale = next; size_changed = true; }
|
||||
}
|
||||
// Manipulation uses the captured controller ray, not coordinates
|
||||
// fed back from a changing overlay or a batch of stale mouse hits.
|
||||
break;
|
||||
case vr::VREvent_MouseButtonDown:
|
||||
++pointer_downs;
|
||||
last_pointer_event = "down button=" + std::to_string(event.data.mouse.button);
|
||||
if (event.data.mouse.button == vr::VRMouseButton_Left)
|
||||
surface.pointer_down(event.data.mouse.cursorIndex, event.data.mouse.x, H - event.data.mouse.y);
|
||||
if (auto kind = surface.pointer_down(event.data.mouse.cursorIndex, event.data.mouse.x, H - event.data.mouse.y))
|
||||
begin_drag(*kind, event);
|
||||
break;
|
||||
case vr::VREvent_MouseButtonUp:
|
||||
++pointer_ups;
|
||||
@@ -412,7 +507,17 @@ struct Overlay::Impl {
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
update_drag();
|
||||
place();
|
||||
if (drag.active()) {
|
||||
// A controller used to manipulate the panel must not also authorize
|
||||
// Record/Insert/Enter. Require neutral rearm after the drag ends.
|
||||
left.reset(); right.reset();
|
||||
if (grip_capture || ptt_capture) result.push_back(UiAction::Cancel);
|
||||
grip_capture = ptt_capture = false;
|
||||
for (auto& edge : edges) edge.reset();
|
||||
return result;
|
||||
}
|
||||
vr::VRActiveActionSet_t set{};
|
||||
set.ulActionSet = action_set;
|
||||
set.ulRestrictedToDevice = vr::k_ulInvalidInputValueHandle;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
#pragma once
|
||||
|
||||
#include "mount.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
|
||||
namespace frameyap {
|
||||
|
||||
// Inputs are rigid poses in the SAME coordinate frame (world or the same
|
||||
// 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; };
|
||||
|
||||
namespace panel_drag_detail {
|
||||
struct Vec3 { double x, y, z; };
|
||||
inline Vec3 operator+(Vec3 a, Vec3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
|
||||
inline Vec3 operator-(Vec3 a, Vec3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
|
||||
inline Vec3 operator*(Vec3 a, double s) { return {a.x * s, a.y * s, a.z * s}; }
|
||||
inline double dot(Vec3 a, Vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
||||
inline Vec3 cross(Vec3 a, Vec3 b) {
|
||||
return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x};
|
||||
}
|
||||
inline bool finite(Vec3 a) { return std::isfinite(a.x) && std::isfinite(a.y) && std::isfinite(a.z); }
|
||||
inline Vec3 column(const Matrix34& m, int c) { return {m[0][c], m[1][c], m[2][c]}; }
|
||||
inline Vec3 translation(const Matrix34& m) { return column(m, 3); }
|
||||
inline Vec3 rotate(const Matrix34& m, Vec3 v) {
|
||||
return column(m, 0) * v.x + column(m, 1) * v.y + column(m, 2) * v.z;
|
||||
}
|
||||
inline Vec3 inverse_rotate(const Matrix34& m, Vec3 v) {
|
||||
return {dot(column(m, 0), v), dot(column(m, 1), v), dot(column(m, 2), v)};
|
||||
}
|
||||
inline bool rigid(const Matrix34& m) {
|
||||
for (const auto& row : m)
|
||||
for (float value : row)
|
||||
if (!std::isfinite(value)) return false;
|
||||
const Vec3 x = column(m, 0), y = column(m, 1), z = column(m, 2);
|
||||
constexpr double tolerance = 1e-3;
|
||||
return std::abs(dot(x, x) - 1) < tolerance &&
|
||||
std::abs(dot(y, y) - 1) < tolerance &&
|
||||
std::abs(dot(z, z) - 1) < tolerance &&
|
||||
std::abs(dot(x, y)) < tolerance &&
|
||||
std::abs(dot(x, z)) < tolerance &&
|
||||
std::abs(dot(y, z)) < tolerance &&
|
||||
std::abs(dot(cross(x, y), z) - 1) < tolerance;
|
||||
}
|
||||
} // namespace panel_drag_detail
|
||||
|
||||
class PanelDrag {
|
||||
public:
|
||||
bool begin(PanelDragKind kind, const Matrix34& panel_pose, float width, float height,
|
||||
float hit_x, float hit_y, const Matrix34& controller_pose) {
|
||||
using namespace panel_drag_detail;
|
||||
reset();
|
||||
if ((kind != PanelDragKind::Grab && kind != PanelDragKind::Scale) ||
|
||||
!rigid(panel_pose) || !rigid(controller_pose) ||
|
||||
!std::isfinite(width) || !std::isfinite(height) || width <= 0 || height <= 0 ||
|
||||
!std::isfinite(hit_x) || !std::isfinite(hit_y) ||
|
||||
hit_x < 0 || hit_x > 1 || hit_y < 0 || hit_y > 1) return false;
|
||||
|
||||
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);
|
||||
const Vec3 anchor = center - right * (static_cast<double>(width) * .5) +
|
||||
up * (static_cast<double>(height) * .5);
|
||||
const Vec3 down_hit = anchor + right * (static_cast<double>(width) * hit_x) -
|
||||
up * (static_cast<double>(height) * hit_y);
|
||||
const Vec3 down_vector = down_hit - translation(controller_pose);
|
||||
const double length = std::sqrt(dot(down_vector, down_vector));
|
||||
if (!std::isfinite(length) || length < 1e-6 || length > 10) return false;
|
||||
const Vec3 direction = down_vector * (1.0 / length);
|
||||
const Vec3 baseline = down_hit - anchor;
|
||||
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)))
|
||||
return false;
|
||||
|
||||
kind_ = kind;
|
||||
center_ = center;
|
||||
right_ = right;
|
||||
up_ = up;
|
||||
normal_ = normal;
|
||||
anchor_ = anchor;
|
||||
down_hit_ = down_hit;
|
||||
baseline_sq_ = baseline_sq;
|
||||
local_ray_ = inverse_rotate(controller_pose, direction);
|
||||
active_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<PanelDragUpdate> update(const Matrix34& controller_pose) const {
|
||||
using namespace panel_drag_detail;
|
||||
if (!active_ || !rigid(controller_pose)) return std::nullopt;
|
||||
const Vec3 origin = translation(controller_pose);
|
||||
const Vec3 ray = rotate(controller_pose, local_ray_);
|
||||
const double denominator = dot(ray, normal_);
|
||||
if (!finite(ray) || !std::isfinite(denominator) || std::abs(denominator) < 1e-5)
|
||||
return std::nullopt;
|
||||
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)};
|
||||
}
|
||||
|
||||
void reset() { active_ = false; }
|
||||
bool active() const { return active_; }
|
||||
|
||||
private:
|
||||
using Vec3 = panel_drag_detail::Vec3;
|
||||
bool active_ = false;
|
||||
PanelDragKind kind_ = PanelDragKind::Grab;
|
||||
Vec3 center_{}, right_{}, up_{}, normal_{}, anchor_{}, down_hit_{}, local_ray_{};
|
||||
double baseline_sq_ = 0;
|
||||
};
|
||||
|
||||
} // namespace frameyap
|
||||
+39
-35
@@ -13,7 +13,7 @@ namespace frameyap {
|
||||
namespace {
|
||||
constexpr int W = PanelSurface::width, H = PanelSurface::height;
|
||||
using Color = Rgba;
|
||||
constexpr int grip_x = 927, grip_y = 646, grip_w = 51, grip_h = 29;
|
||||
constexpr int CW = PanelSurface::body.w, CH = PanelSurface::body.h;
|
||||
float rounded_distance(float px, float py, int x, int y, int w, int h, float radius) {
|
||||
const float dx = std::abs(px - (x + w / 2.f)) - (w / 2.f - radius);
|
||||
const float dy = std::abs(py - (y + h / 2.f)) - (h / 2.f - radius);
|
||||
@@ -111,8 +111,7 @@ struct PanelSurface::Impl {
|
||||
bool dirty = true, lasers_anytime = false, advanced_debug = false, auto_insert = false;
|
||||
std::string placement_note, binding_note;
|
||||
std::array<int, 2> pressed{{-1, -1}};
|
||||
int resize_cursor = -1;
|
||||
float resize_x = 0.f, resize_y = 0.f;
|
||||
int drag_cursor = -1;
|
||||
std::vector<std::string> lines;
|
||||
size_t page = 0;
|
||||
static constexpr size_t lines_per_page = 4;
|
||||
@@ -144,8 +143,14 @@ struct PanelSurface::Impl {
|
||||
}
|
||||
void blend(int x, int y, Color color, float amount) {
|
||||
auto* dst = pixels.data() + (size_t(y) * W + x) * 4;
|
||||
// Straight-alpha source-over, including strokes in the transparent margin.
|
||||
const float alpha = amount * color[3] / 255.f;
|
||||
const float old_alpha = dst[3] / 255.f;
|
||||
const float out_alpha = alpha + old_alpha * (1.f - alpha);
|
||||
if (out_alpha <= 0.f) return;
|
||||
for (int k = 0; k < 3; ++k)
|
||||
dst[k] = static_cast<unsigned char>(dst[k] * (1.f - amount) + color[k] * amount);
|
||||
dst[k] = static_cast<unsigned char>((dst[k] * old_alpha * (1.f - alpha) + color[k] * alpha) / out_alpha);
|
||||
dst[3] = static_cast<unsigned char>(255.f * out_alpha);
|
||||
}
|
||||
void rounded(Rect r, int radius, Color fill, Color edge, float glow = 0.f, int stroke = 1) {
|
||||
// Distance-field antialiasing and restrained baked glow. No second texture or GPU pass.
|
||||
@@ -166,12 +171,12 @@ struct PanelSurface::Impl {
|
||||
void frame() {
|
||||
// Independently rasterized version of kouseki's HUD visual language:
|
||||
// rounded mint-to-blue perimeter and a second shallow curved accent.
|
||||
for (int y = 0; y < H; ++y) for (int x = 0; x < W; ++x) {
|
||||
if (x > 34 && x < W - 34 && y > 34 && y < H - 34) continue;
|
||||
const float distance = rounded_distance(x, y, 10, 10, W - 20, H - 20, 18.f);
|
||||
for (int y = 0; y < CH; ++y) for (int x = 0; x < CW; ++x) {
|
||||
if (x > 34 && x < CW - 34 && y > 34 && y < CH - 34) continue;
|
||||
const float distance = rounded_distance(x, y, 10, 10, CW - 20, CH - 20, 18.f);
|
||||
const float edge = std::abs(distance);
|
||||
const float strength = edge <= 1.f ? 1.f : .30f * std::max(0.f, 1.f - (edge - 1.f) / 6.f);
|
||||
const float t = float(x) / W;
|
||||
const float t = float(x) / CW;
|
||||
Color gradient{};
|
||||
for (int k = 0; k < 3; ++k)
|
||||
gradient[k] = static_cast<unsigned char>(theme.frame_start[k] * (1.f - t) + theme.frame_end[k] * t);
|
||||
@@ -180,9 +185,9 @@ struct PanelSurface::Impl {
|
||||
for (int k = 0; k < 3; ++k) dst[k] = static_cast<unsigned char>(background[k] + (gradient[k] - background[k]) * strength);
|
||||
dst[3] = distance <= 1.f ? 255 : static_cast<unsigned char>(255 * std::max(0.f, 1.f - (distance - 1.f) / 6.f));
|
||||
}
|
||||
for (int x = 32; x < W - 32; ++x) {
|
||||
const float t = float(x - 32) / (W - 64);
|
||||
const int y = H - 21 - int(5 * std::sin(t * 3.14159265f));
|
||||
for (int x = 32; x < CW - 32; ++x) {
|
||||
const float t = float(x - 32) / (CW - 64);
|
||||
const int y = CH - 21 - int(5 * std::sin(t * 3.14159265f));
|
||||
Color gradient{};
|
||||
for (int k = 0; k < 3; ++k)
|
||||
gradient[k] = static_cast<unsigned char>(theme.frame_start[k] * (1.f - t) + theme.frame_end[k] * t);
|
||||
@@ -266,7 +271,7 @@ struct PanelSurface::Impl {
|
||||
}
|
||||
void reset() {
|
||||
pressed.fill(-1);
|
||||
resize_cursor = -1;
|
||||
drag_cursor = -1;
|
||||
}
|
||||
bool render(const Panel& p) {
|
||||
if (p.recording != panel.recording || p.enabled != panel.enabled ||
|
||||
@@ -282,7 +287,8 @@ struct PanelSurface::Impl {
|
||||
dirty = true;
|
||||
panel = p;
|
||||
if (!dirty) return false;
|
||||
rect({0, 0, W, H}, background);
|
||||
std::fill(pixels.begin(), pixels.end(), 0);
|
||||
rect({0, 0, CW, CH}, background);
|
||||
frame();
|
||||
text("FrameYap", 32, 61, 40, ink, 300);
|
||||
text(auto_insert ? "ON-DEVICE / AUTO INSERT OPT-IN" : "ON-DEVICE / REVIEW FIRST", 280, 58, 22, muted, 720);
|
||||
@@ -341,11 +347,12 @@ struct PanelSurface::Impl {
|
||||
active ? cyan : muted, b.r.x + b.r.w - 12);
|
||||
}
|
||||
}
|
||||
// Separate from the Quit hitbox. The three diagonal marks remain
|
||||
// visible on both tabs, slightly inset from the lower-right corner.
|
||||
for (int n = 0; n < 3; ++n)
|
||||
for (int i = 0; i < 13 + 5 * n; ++i)
|
||||
rect({956 - i, 657 + 5 * n + i / 3, 2, 2}, cyan);
|
||||
// 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);
|
||||
dirty = false;
|
||||
return true;
|
||||
}
|
||||
@@ -356,30 +363,27 @@ PanelSurface::~PanelSurface() = default;
|
||||
bool PanelSurface::render(const Panel& p) { return impl_->render(p); }
|
||||
const std::vector<unsigned char>& PanelSurface::pixels() const { return impl_->pixels; }
|
||||
bool PanelSurface::available(UiAction a) const { return impl_->available(a); }
|
||||
std::optional<float> PanelSurface::pointer_move(unsigned cursor, float x, float y) {
|
||||
if (cursor >= impl_->pressed.size() || int(cursor) != impl_->resize_cursor ||
|
||||
!std::isfinite(x) || !std::isfinite(y)) return {};
|
||||
// OpenVR supplies coordinates on the *current* panel. Work incrementally
|
||||
// so resizing never depends on a stale down-position after a width change.
|
||||
const float delta = (x - impl_->resize_x) / W - (y - impl_->resize_y) / H;
|
||||
impl_->resize_x = x; impl_->resize_y = y;
|
||||
return std::clamp(1.f + delta, .9f, 1.1f);
|
||||
bool PanelSurface::dragging(unsigned cursor) const {
|
||||
return cursor < impl_->pressed.size() && int(cursor) == impl_->drag_cursor;
|
||||
}
|
||||
void PanelSurface::pointer_down(unsigned cursor, float x, float y) {
|
||||
if (cursor >= impl_->pressed.size()) return;
|
||||
if (impl_->resize_cursor < 0 && std::isfinite(x) && std::isfinite(y) &&
|
||||
x >= grip_x && x < grip_x + grip_w && y >= grip_y && y < grip_y + grip_h) {
|
||||
impl_->resize_cursor = int(cursor);
|
||||
impl_->resize_x = x; impl_->resize_y = y;
|
||||
impl_->pressed[cursor] = -1;
|
||||
return;
|
||||
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)) {
|
||||
impl_->reset(); // other cursor's prior approval cannot survive relocation
|
||||
impl_->drag_cursor = int(cursor);
|
||||
return contains(grab) ? PanelDragKind::Grab : PanelDragKind::Scale;
|
||||
}
|
||||
impl_->pressed[cursor] = impl_->hit(x, y);
|
||||
return {};
|
||||
}
|
||||
SurfaceEvent PanelSurface::pointer_up(unsigned cursor, float x, float y) {
|
||||
SurfaceEvent result;
|
||||
if (cursor >= impl_->pressed.size()) return result;
|
||||
if (impl_->resize_cursor == int(cursor)) { impl_->resize_cursor = -1; return result; }
|
||||
if (impl_->drag_cursor >= 0) {
|
||||
if (impl_->drag_cursor == int(cursor)) impl_->reset();
|
||||
return result;
|
||||
}
|
||||
int index = std::exchange(impl_->pressed[cursor], -1);
|
||||
if (index < 0 || impl_->hit(x, y) != index) return result;
|
||||
auto c = buttons[index].id;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "overlay.hpp"
|
||||
#include "config.hpp"
|
||||
#include "panel_drag.hpp"
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -20,17 +21,21 @@ struct SurfaceEvent {
|
||||
// status and safety controls remain on the same surface.
|
||||
class PanelSurface {
|
||||
public:
|
||||
static constexpr int width = 1000, height = 680;
|
||||
struct Bounds { int x, y, w, h; };
|
||||
static constexpr int width = 1080, height = 780;
|
||||
static constexpr Bounds body{0, 0, 1000, 680};
|
||||
static constexpr Bounds grab{370, 698, 260, 52};
|
||||
static constexpr Bounds scale{996, 680, 68, 68};
|
||||
PanelSurface(const std::string& font, Mount mount, Theme theme = {});
|
||||
~PanelSurface();
|
||||
PanelSurface(const PanelSurface&) = delete;
|
||||
PanelSurface& operator=(const PanelSurface&) = delete;
|
||||
bool render(const Panel& panel);
|
||||
const std::vector<unsigned char>& pixels() const;
|
||||
void pointer_down(unsigned cursor, float x, float y);
|
||||
// Handles capture one cursor and never authorize a UI action on release.
|
||||
std::optional<PanelDragKind> pointer_down(unsigned cursor, float x, float y);
|
||||
SurfaceEvent pointer_up(unsigned cursor, float x, float y);
|
||||
// Returns a relative physical-width change while dragging the resize grip.
|
||||
std::optional<float> pointer_move(unsigned cursor, float x, float y);
|
||||
bool dragging(unsigned cursor) const;
|
||||
void reset_pointers();
|
||||
void set_placement_note(std::string note);
|
||||
void set_lasers_anytime(bool enabled);
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#include "panel_drag.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
using namespace frameyap;
|
||||
namespace {
|
||||
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); }
|
||||
Matrix34 translated(Matrix34 pose, float x, float y, float z) {
|
||||
pose[0][3] += x; pose[1][3] += y; pose[2][3] += z;
|
||||
return pose;
|
||||
}
|
||||
Matrix34 controller_at(float x, float y, float z = 1.f) {
|
||||
return translated(identity, x, y, z);
|
||||
}
|
||||
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.
|
||||
const auto down = controller_at(x - .5f, .5f - y);
|
||||
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);
|
||||
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);
|
||||
}
|
||||
// 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}}}};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
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));
|
||||
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);
|
||||
drag.reset(); assert(!drag.active()); assert(!drag.update(down));
|
||||
|
||||
// Independent horizontal, vertical, and diagonal anchor-based scaling;
|
||||
// both contraction and growth, including intersections outside the panel.
|
||||
for (float factor : {.4f, 1.f, 1.6f, 3.f}) {
|
||||
scale_case(.5f, 0.f, factor);
|
||||
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(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
|
||||
assert(moved); near(moved->factor, 1);
|
||||
drag.reset();
|
||||
assert(!drag.begin(PanelDragKind::Scale, identity, 1, 1, 0, 0,
|
||||
controller_at(-.5f, .5f))); // zero anchor baseline
|
||||
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.
|
||||
auto angled_down = controller_at(0, 0);
|
||||
assert(drag.begin(PanelDragKind::Grab, 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);
|
||||
|
||||
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));
|
||||
moved = drag.update(translated(corner_ray, 0, -.25f, -.25f));
|
||||
assert(moved); near(moved->factor, 2.f);
|
||||
|
||||
// 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);
|
||||
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);
|
||||
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);
|
||||
|
||||
const float nan = std::numeric_limits<float>::quiet_NaN();
|
||||
const float inf = std::numeric_limits<float>::infinity();
|
||||
auto bad = identity; bad[2][3] = nan;
|
||||
assert(!drag.begin(PanelDragKind::Grab, bad, 1, 1, .5f, .5f, down));
|
||||
assert(!drag.active()); assert(!drag.update(down));
|
||||
bad = identity; bad[0][0] = 2; // not rigid
|
||||
assert(!drag.begin(PanelDragKind::Grab, bad, 1, 1, .5f, .5f, down));
|
||||
bad = down; bad[0][3] = inf;
|
||||
assert(!drag.begin(PanelDragKind::Grab, identity, 1, 1, .5f, .5f, bad));
|
||||
bad = down; bad[0][0] = -1; // reflection, not a proper rotation
|
||||
assert(!drag.begin(PanelDragKind::Grab, identity, 1, 1, .5f, .5f, bad));
|
||||
assert(!drag.begin(PanelDragKind::Grab, identity, 0, 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, -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,
|
||||
controller_at(1.f, 0.f, 1e-7f))); // near parallel at down
|
||||
assert(!drag.begin(PanelDragKind::Grab, 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));
|
||||
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.active()); // invalid update leaves original calibration intact
|
||||
assert(drag.update(down));
|
||||
drag.reset(); assert(!drag.active());
|
||||
}
|
||||
+28
-14
@@ -61,19 +61,34 @@ int main(int argc, char** argv) {
|
||||
no_action(click(surface, std::numeric_limits<float>::quiet_NaN(), 610));
|
||||
no_action(click(surface, -1, 610));
|
||||
no_action(click(surface, 1000, 680));
|
||||
// Lower-right grip scales without activating the nearby Quit button or
|
||||
// uploading new pixels. Other cursors cannot hijack an active drag.
|
||||
surface.pointer_down(0, 951, 656);
|
||||
assert(!surface.pointer_move(1, 970, 650));
|
||||
auto factor = surface.pointer_move(0, 971, 641);
|
||||
assert(factor && *factor > 1.f);
|
||||
// Outside handles capture one cursor, invalidate all previous approvals,
|
||||
// and never activate Quit/Record on release. No repaint during manipulation.
|
||||
assert(surface.pointer_down(0, 1028, 712) == PanelDragKind::Scale);
|
||||
assert(surface.dragging(0) && !surface.dragging(1));
|
||||
assert(!surface.pointer_down(1, 500, 724));
|
||||
no_action(surface.pointer_up(1, 1028, 712));
|
||||
assert(surface.dragging(0));
|
||||
no_action(surface.pointer_up(0, 900, 610));
|
||||
assert(!surface.pointer_move(0, 975, 640));
|
||||
assert(!surface.dragging(0));
|
||||
assert(!surface.render(p));
|
||||
surface.pointer_down(0, 951, 656);
|
||||
surface.pointer_down(1, 100, 610);
|
||||
assert(surface.pointer_down(0, 500, 724) == PanelDragKind::Grab);
|
||||
no_action(surface.pointer_up(1, 100, 610));
|
||||
no_action(surface.pointer_up(0, 100, 610));
|
||||
no_action(surface.pointer_up(1, 100, 610));
|
||||
assert(surface.pointer_down(0, 1028, 712) == PanelDragKind::Scale);
|
||||
surface.reset_pointers();
|
||||
assert(!surface.pointer_move(0, 971, 641));
|
||||
no_action(surface.pointer_up(0, 951, 656));
|
||||
assert(!surface.dragging(0));
|
||||
no_action(surface.pointer_up(0, 1028, 712));
|
||||
no_action(click(surface, 951, 656)); // retired inset grip
|
||||
no_action(click(surface, 800, 724)); // empty transparent margin
|
||||
assert(!surface.pointer_down(0, 996, 680)); // clipped handle corner
|
||||
const auto alpha = [&](int x, int y) { return surface.pixels()[(y * PanelSurface::width + x) * 4 + 3]; };
|
||||
assert(alpha(500, 723) == 255 && alpha(1040, 705) == 255);
|
||||
assert(alpha(800, 724) == 0 && alpha(1050, 760) == 0);
|
||||
assert(alpha(500, 690) == 0 && alpha(1030, 704) == 0);
|
||||
// Antialiased handle edges carry real alpha, not RGB hidden at alpha zero.
|
||||
assert(alpha(401, 721) > 0 && alpha(401, 721) < 255);
|
||||
|
||||
p.enabled = true;
|
||||
p.record_available = false;
|
||||
@@ -207,10 +222,9 @@ int main(int argc, char** argv) {
|
||||
assert(surface.pixels() == replaced);
|
||||
p.status = std::string(4096, 's'); p.detail = std::string(4096, 'd');
|
||||
assert(surface.render(p)); assert(!surface.render(p));
|
||||
// Laser motion and a press do not re-upload raw pixels. A completed
|
||||
// action still reaches the caller; meaningful panel changes redraw.
|
||||
surface.pointer_move(0, 900, 610); assert(!surface.render(p));
|
||||
surface.pointer_move(0, 900, 610); assert(!surface.render(p));
|
||||
// A press does not re-upload pixels. A completed action still reaches the
|
||||
// caller; meaningful panel changes redraw.
|
||||
assert(!surface.render(p));
|
||||
surface.pointer_down(0, 900, 610); assert(!surface.render(p));
|
||||
assert(surface.pointer_up(0, 900, 610).action == UiAction::Quit);
|
||||
assert(!surface.render(p));
|
||||
|
||||
Reference in new issue
Block a user