From 5b40032bc3889255cb14dabf7e41e811593e70c8 Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:48:06 +0900 Subject: [PATCH] anchor grab --- wlx-overlay-s/src/backend/input.rs | 196 +++++++++++++++---------- wlx-overlay-s/src/overlays/anchor.rs | 6 +- wlx-overlay-s/src/overlays/edit/mod.rs | 4 +- wlx-overlay-s/src/overlays/edit/pos.rs | 2 +- wlx-overlay-s/src/state.rs | 2 + wlx-overlay-s/src/windowing/window.rs | 98 ++++++------- 6 files changed, 177 insertions(+), 131 deletions(-) diff --git a/wlx-overlay-s/src/backend/input.rs b/wlx-overlay-s/src/backend/input.rs index 2749211..646e225 100644 --- a/wlx-overlay-s/src/backend/input.rs +++ b/wlx-overlay-s/src/backend/input.rs @@ -11,7 +11,7 @@ use crate::state::{AppSession, AppState}; use crate::subsystem::hid::WheelDelta; use crate::subsystem::input::KeyboardFocus; use crate::windowing::manager::OverlayWindowManager; -use crate::windowing::window::{OverlayWindowData, OverlayWindowState, Positioning}; +use crate::windowing::window::{realign, OverlayWindowData, OverlayWindowState, Positioning}; use crate::windowing::{OverlayID, OverlaySelector}; use super::task::{TaskContainer, TaskType}; @@ -274,8 +274,7 @@ struct RayHit { pub struct GrabData { pub offset: Vec3A, pub grabbed_id: OverlayID, - pub old_curvature: Option, - pub grab_all: bool, + pub grab_anchor: bool, } #[repr(u8)] @@ -327,7 +326,7 @@ where let mut pointer = &mut app.input_state.pointers[idx]; if let Some(grab_data) = pointer.interaction.grabbed { if let Some(grabbed) = overlays.mut_by_id(grab_data.grabbed_id) { - Pointer::handle_grabbed(idx, grabbed, app); + handle_grabbed(idx, grabbed, app); } else { log::warn!("Grabbed overlay {:?} does not exist", grab_data.grabbed_id); pointer.interaction.grabbed = None; @@ -356,6 +355,8 @@ where } overlays.edit_overlay(hit.overlay, true, app); + let edit_mode = overlays.get_edit_mode(); + let Some(hovered) = overlays.mut_by_id(hit.overlay) else { log::warn!("Hit overlay {:?} does not exist", hit.overlay); return (0.0, None); // no hit @@ -384,7 +385,7 @@ where &mut app.hid_provider.keyboard_focus, hovered.config.keyboard_focus, ); - pointer.start_grab(hit.overlay, hovered_state, &mut app.tasks); + start_grab(idx, hit.overlay, hovered_state, app, edit_mode); log::debug!("Hand {}: grabbed {}", hit.pointer, hovered.config.name); return ( hit.dist, @@ -581,94 +582,137 @@ where (None, None) } -impl Pointer { - fn start_grab( - &mut self, - id: OverlayID, - state: &mut OverlayWindowState, - tasks: &mut TaskContainer, - ) { - let offset = self - .pose - .inverse() - .transform_point3a(state.transform.translation); +fn start_grab( + idx: usize, + id: OverlayID, + state: &mut OverlayWindowState, + app: &mut AppState, + edit_mode: bool, +) { + let pointer = &mut app.input_state.pointers[idx]; - self.interaction.grabbed = Some(GrabData { - offset, - grabbed_id: id, - old_curvature: state.curvature, - grab_all: matches!(self.interaction.mode, PointerMode::Right), - }); - state.positioning = match state.positioning { - Positioning::FollowHand { hand, lerp } => Positioning::FollowHandPaused { hand, lerp }, - Positioning::FollowHead { lerp } => Positioning::FollowHeadPaused { lerp }, - x => x, - }; + // Grab anchor if: + // - grabbed overlay is Anchored + // - not in editmode + // - grabbing with one hand. (grabbing with the 2nd hand will grab the individual overlay instead) + let grab_anchor = + !edit_mode && !app.anchor_grabbed && matches!(state.positioning, Positioning::Anchored); - // Show anchor - tasks.enqueue(TaskType::Overlay( - OverlaySelector::Name(ANCHOR_NAME.clone()), - Box::new(|app, o| { - o.activate_static(app.anchor * Affine3A::from_scale(Vec3::ONE * 0.1)); - }), - )); + let relative_grab_point = if grab_anchor { + app.anchor.translation + } else { + state.transform.translation + }; + + let offset = pointer + .pose + .inverse() + .transform_point3a(relative_grab_point); + + app.anchor_grabbed = grab_anchor; + + pointer.interaction.grabbed = Some(GrabData { + offset, + grabbed_id: id, + grab_anchor, + }); + + state.positioning = match state.positioning { + Positioning::FollowHand { hand, lerp } => Positioning::FollowHandPaused { hand, lerp }, + Positioning::FollowHead { lerp } => Positioning::FollowHeadPaused { lerp }, + Positioning::Anchored if !grab_anchor => Positioning::AnchoredPaused, + x => x, + }; + + // Show anchor + app.tasks.enqueue(TaskType::Overlay( + OverlaySelector::Name(ANCHOR_NAME.clone()), + Box::new(|app, o| { + o.activate(app); + }), + )); +} + +fn handle_scale(transform: &mut Affine3A, scroll_y: f32) { + let cur_scale = transform.x_axis.length(); + if cur_scale < 0.1 && scroll_y > 0.0 { + return; + } + if cur_scale > 20. && scroll_y < 0.0 { + return; } - fn handle_grabbed(idx: usize, overlay: &mut OverlayWindowData, app: &mut AppState) - where - O: Default, - { - let Some(overlay_state) = overlay.config.active_state.as_mut() else { - return; - }; + transform.matrix3 = transform + .matrix3 + .mul_scalar(0.025f32.mul_add(-scroll_y, 1.0)); +} - let pointer = &mut app.input_state.pointers[idx]; - if pointer.now.grab { - if let Some(grab_data) = pointer.interaction.grabbed.as_mut() { - if pointer.now.click { - pointer.interaction.mode = PointerMode::Special; - let cur_scale = overlay_state.transform.x_axis.length(); - if cur_scale < 0.1 && pointer.now.scroll_y > 0.0 { - return; - } - if cur_scale > 20. && pointer.now.scroll_y < 0.0 { - return; - } +fn handle_grabbed(idx: usize, overlay: &mut OverlayWindowData, app: &mut AppState) +where + O: Default, +{ + let pointer = &mut app.input_state.pointers[idx]; + let Some(grab_data) = pointer.interaction.grabbed.as_mut() else { + log::error!("Grabbed overlay does not exist"); + return; + }; + let grab_anchor = grab_data.grab_anchor; - overlay_state.transform.matrix3 = overlay_state - .transform - .matrix3 - .mul_scalar(0.025f32.mul_add(-pointer.now.scroll_y, 1.0)); - } else if app.session.config.allow_sliding && pointer.now.scroll_y.is_finite() { - grab_data.offset.z -= pointer.now.scroll_y * 0.05; - } - overlay_state.transform.translation = - pointer.pose.transform_point3a(grab_data.offset); - overlay.config.realign(&app.input_state.hmd); - } else { - log::error!("Grabbed overlay does not exist"); - pointer.interaction.grabbed = None; + let Some(overlay_state) = overlay.config.active_state.as_mut() else { + return; + }; + + if pointer.now.grab { + if grab_anchor { + if pointer.now.click { + pointer.interaction.mode = PointerMode::Special; + handle_scale(&mut app.anchor, pointer.now.scroll_y); + } else if app.session.config.allow_sliding && pointer.now.scroll_y.is_finite() { + // single grab push/pull + grab_data.offset.z -= pointer.now.scroll_y * 0.05; } + app.anchor.translation = pointer.pose.transform_point3a(grab_data.offset); + realign(&mut app.anchor, &app.input_state.hmd); } else { + // single grab resize + if pointer.now.click { + pointer.interaction.mode = PointerMode::Special; + handle_scale(&mut overlay_state.transform, pointer.now.scroll_y); + } else if app.session.config.allow_sliding && pointer.now.scroll_y.is_finite() { + // single grab push/pull + grab_data.offset.z -= pointer.now.scroll_y * 0.05; + } + overlay_state.transform.translation = pointer.pose.transform_point3a(grab_data.offset); + realign(&mut overlay_state.transform, &app.input_state.hmd); + overlay.config.dirty = true; + } + } else { + // not now.grab + pointer.interaction.grabbed = None; + if grab_anchor { + app.anchor_grabbed = false; + } else { + // single grab released overlay_state.positioning = match overlay_state.positioning { Positioning::FollowHandPaused { hand, lerp } => { Positioning::FollowHand { hand, lerp } } Positioning::FollowHeadPaused { lerp } => Positioning::FollowHead { lerp }, + Positioning::AnchoredPaused => Positioning::Anchored, x => x, }; - pointer.interaction.grabbed = None; - overlay_state.save_transform(app); - // Hide anchor - app.tasks.enqueue(TaskType::Overlay( - OverlaySelector::Name(ANCHOR_NAME.clone()), - Box::new(|_app, o| { - o.deactivate(); - }), - )); - log::debug!("Hand {}: dropped {}", idx, overlay.config.name); + overlay_state.save_transform(app); } + + // Hide anchor + app.tasks.enqueue(TaskType::Overlay( + OverlaySelector::Name(ANCHOR_NAME.clone()), + Box::new(|_app, o| { + o.deactivate(); + }), + )); + log::debug!("Hand {}: dropped {}", idx, overlay.config.name); } } diff --git a/wlx-overlay-s/src/overlays/anchor.rs b/wlx-overlay-s/src/overlays/anchor.rs index 9f8bc09..14300eb 100644 --- a/wlx-overlay-s/src/overlays/anchor.rs +++ b/wlx-overlay-s/src/overlays/anchor.rs @@ -3,8 +3,8 @@ use std::sync::{Arc, LazyLock}; use crate::gui::panel::GuiPanel; use crate::state::AppState; -use crate::windowing::Z_ORDER_ANCHOR; use crate::windowing::window::{OverlayWindowConfig, OverlayWindowState, Positioning}; +use crate::windowing::Z_ORDER_ANCHOR; pub static ANCHOR_NAME: LazyLock> = LazyLock::new(|| Arc::from("anchor")); @@ -18,11 +18,11 @@ pub fn create_anchor(app: &mut AppState) -> anyhow::Result default_state: OverlayWindowState { interactable: false, grabbable: false, - positioning: Positioning::Static, + positioning: Positioning::Anchored, transform: Affine3A::from_scale_rotation_translation( Vec3::ONE * 0.1, Quat::IDENTITY, - Vec3::NEG_Z * 0.5, + Vec3::ZERO, // Vec3::NEG_Z * 0.5, ), ..OverlayWindowState::default() }, diff --git a/wlx-overlay-s/src/overlays/edit/mod.rs b/wlx-overlay-s/src/overlays/edit/mod.rs index 87a4530..fb383e1 100644 --- a/wlx-overlay-s/src/overlays/edit/mod.rs +++ b/wlx-overlay-s/src/overlays/edit/mod.rs @@ -19,16 +19,16 @@ use wgui::{ use crate::{backend::task::TaskType, windowing::OverlaySelector}; use crate::{ backend::{input::HoverResult, task::TaskContainer}, - gui::panel::{GuiPanel, NewGuiPanelParams, OnCustomAttribFunc, button::BUTTON_EVENTS}, + gui::panel::{button::BUTTON_EVENTS, GuiPanel, NewGuiPanelParams, OnCustomAttribFunc}, overlays::edit::{ lock::InteractLockHandler, pos::PositioningHandler, tab::ButtonPaneTabSwitcher, }, state::AppState, subsystem::hid::WheelDelta, windowing::{ - OverlayID, backend::{DummyBackend, OverlayBackend, RenderResources, ShouldRender}, window::OverlayWindowConfig, + OverlayID, }, }; diff --git a/wlx-overlay-s/src/overlays/edit/pos.rs b/wlx-overlay-s/src/overlays/edit/pos.rs index b86b9e1..6a5d021 100644 --- a/wlx-overlay-s/src/overlays/edit/pos.rs +++ b/wlx-overlay-s/src/overlays/edit/pos.rs @@ -131,7 +131,7 @@ fn key_to_pos(key: &str) -> Positioning { const fn pos_to_key(pos: Positioning) -> &'static str { match pos { Positioning::Static => "static", - Positioning::Anchored => "anchored", + Positioning::Anchored | Positioning::AnchoredPaused => "anchored", Positioning::Floating => "floating", Positioning::FollowHead { .. } | Positioning::FollowHeadPaused { .. } => "hmd", Positioning::FollowHand { diff --git a/wlx-overlay-s/src/state.rs b/wlx-overlay-s/src/state.rs index a72e0ed..11bb93e 100644 --- a/wlx-overlay-s/src/state.rs +++ b/wlx-overlay-s/src/state.rs @@ -42,6 +42,7 @@ pub struct AppState { pub input_state: InputState, pub screens: SmallVec<[ScreenMeta; 8]>, pub anchor: Affine3A, + pub anchor_grabbed: bool, pub toast_sound: &'static [u8], pub wgui_globals: WguiGlobals, @@ -94,6 +95,7 @@ impl AppState { input_state: InputState::new(), screens: smallvec![], anchor: Affine3A::IDENTITY, + anchor_grabbed: false, toast_sound: toast_sound_wav, wgui_globals: WguiGlobals::new( Box::new(gui::asset::GuiAsset {}), diff --git a/wlx-overlay-s/src/windowing/window.rs b/wlx-overlay-s/src/windowing/window.rs index 10446d4..4916f16 100644 --- a/wlx-overlay-s/src/windowing/window.rs +++ b/wlx-overlay-s/src/windowing/window.rs @@ -16,8 +16,10 @@ pub enum Positioning { /// Stays in place, recenters relative to HMD #[default] Floating, - /// Stays in place, recenters relative to anchor + /// Stays in place, recenters relative to anchor. Follows anchor during anchor grab. Anchored, + /// Same as anchor but paused due to interaction + AnchoredPaused, /// Stays in place, no recentering Static, /// Following HMD @@ -151,15 +153,17 @@ impl OverlayWindowConfig { .saved_transform .unwrap_or(self.default_state.transform); - let (target_transform, lerp) = match state.positioning { - Positioning::FollowHead { lerp } => (app.input_state.hmd * cur_transform, lerp), - Positioning::FollowHand { hand, lerp } => ( - app.input_state.pointers[hand as usize].pose * cur_transform, - lerp, - ), + let (parent_transform, lerp) = match state.positioning { + Positioning::FollowHead { lerp } => (app.input_state.hmd, lerp), + Positioning::FollowHand { hand, lerp } => { + (app.input_state.pointers[hand as usize].pose, lerp) + } + Positioning::Anchored => (app.anchor, 1.0), _ => return, }; + let target_transform = parent_transform * cur_transform; + state.transform = match lerp { 1.0 => target_transform, lerp => { @@ -201,7 +205,7 @@ impl OverlayWindowConfig { Positioning::FollowHand { hand, .. } | Positioning::FollowHandPaused { hand, .. } => { app.input_state.pointers[hand as usize].pose } - Positioning::Anchored => app.anchor, + Positioning::Anchored | Positioning::AnchoredPaused => app.anchor, Positioning::Static => return, }; @@ -212,55 +216,49 @@ impl OverlayWindowConfig { state.transform = parent_transform * cur_transform; if state.grabbable && hard_reset { - self.realign(&app.input_state.hmd); + realign(&mut state.transform, &app.input_state.hmd); } self.dirty = true; } +} - pub fn realign(&mut self, hmd: &Affine3A) { - let Some(state) = self.active_state.as_mut() else { - return; - }; +pub fn realign(transform: &mut Affine3A, hmd: &Affine3A) { + let to_hmd = hmd.translation - transform.translation; + let up_dir: Vec3A; - let to_hmd = hmd.translation - state.transform.translation; - let up_dir: Vec3A; + if hmd.x_axis.dot(Vec3A::Y).abs() > 0.2 { + // Snap upright + up_dir = hmd.y_axis; + } else { + let dot = to_hmd.normalize().dot(hmd.z_axis); + let z_dist = to_hmd.length(); + let y_dist = (transform.translation.y - hmd.translation.y).abs(); + let x_angle = (y_dist / z_dist).asin(); - if hmd.x_axis.dot(Vec3A::Y).abs() > 0.2 { - // Snap upright - up_dir = hmd.y_axis; + if dot < -f32::EPSILON { + // facing down + let up_point = hmd.translation + z_dist / x_angle.cos() * Vec3A::Y; + up_dir = (up_point - transform.translation).normalize(); + } else if dot > f32::EPSILON { + // facing up + let dn_point = hmd.translation + z_dist / x_angle.cos() * Vec3A::NEG_Y; + up_dir = (transform.translation - dn_point).normalize(); } else { - let dot = to_hmd.normalize().dot(hmd.z_axis); - let z_dist = to_hmd.length(); - let y_dist = (state.transform.translation.y - hmd.translation.y).abs(); - let x_angle = (y_dist / z_dist).asin(); - - if dot < -f32::EPSILON { - // facing down - let up_point = hmd.translation + z_dist / x_angle.cos() * Vec3A::Y; - up_dir = (up_point - state.transform.translation).normalize(); - } else if dot > f32::EPSILON { - // facing up - let dn_point = hmd.translation + z_dist / x_angle.cos() * Vec3A::NEG_Y; - up_dir = (state.transform.translation - dn_point).normalize(); - } else { - // perfectly upright - up_dir = Vec3A::Y; - } + // perfectly upright + up_dir = Vec3A::Y; } - - let scale = state.transform.x_axis.length(); - - let col_z = (state.transform.translation - hmd.translation).normalize(); - let col_y = up_dir; - let col_x = col_y.cross(col_z); - let col_y = col_z.cross(col_x).normalize(); - let col_x = col_x.normalize(); - - let rot = Mat3A::from_quat(Quat::from_axis_angle(Vec3::Y, PI)); - state.transform.matrix3 = Mat3A::from_cols(col_x, col_y, col_z).mul_scalar(scale) * rot; - - self.dirty = true; } + + let scale = transform.x_axis.length(); + + let col_z = (transform.translation - hmd.translation).normalize(); + let col_y = up_dir; + let col_x = col_y.cross(col_z); + let col_y = col_z.cross(col_x).normalize(); + let col_x = col_x.normalize(); + + let rot = Mat3A::from_quat(Quat::from_axis_angle(Vec3::Y, PI)); + transform.matrix3 = Mat3A::from_cols(col_x, col_y, col_z).mul_scalar(scale) * rot; } // Contains the window state for a given set @@ -302,7 +300,9 @@ impl OverlayWindowState { Positioning::FollowHand { hand, .. } | Positioning::FollowHandPaused { hand, .. } => { app.input_state.pointers[hand as usize].pose } - Positioning::Anchored => snap_upright(app.anchor, Vec3A::Y), + Positioning::Anchored | Positioning::AnchoredPaused => { + snap_upright(app.anchor, Vec3A::Y) + } Positioning::Static => return false, };