feat: window snap angles

This commit is contained in:
galister 2026-07-14 18:04:36 +09:00
parent 779baaf86c
commit 30d786eaa1
14 changed files with 73 additions and 17 deletions

View File

@ -192,7 +192,9 @@
"APPLY_CHANGES_RESTART": "WayVR neu starten, um die Änderungen zu übernehmen?", "APPLY_CHANGES_RESTART": "WayVR neu starten, um die Änderungen zu übernehmen?",
"COLOR_PALETTE_HELP": "Klicken, um Farbpaletten zu durchsuchen.\nUm eigene zu erstellen, besuche die Webseite!", "COLOR_PALETTE_HELP": "Klicken, um Farbpaletten zu durchsuchen.\nUm eigene zu erstellen, besuche die Webseite!",
"COLOR_PALETTE_CUSTOM_URL": "Füge dein eigenes benutzerdefiniertes Thema hinzu!", "COLOR_PALETTE_CUSTOM_URL": "Füge dein eigenes benutzerdefiniertes Thema hinzu!",
"COLOR_PALETTE_CUSTOM_URL2": "Klicken Sie hier, um das Web-Handbuch zu öffnen." "COLOR_PALETTE_CUSTOM_URL2": "Klicken Sie hier, um das Web-Handbuch zu öffnen.",
"SNAP_ANGLE_DEG": "Snap-Winkel für Fensterneigung und Rollwinkel",
"SNAP_ANGLE_DEG_HELP": "Gegriffene Fenster rasten in diesem Winkel auf der Nick- und Rollachse ein."
}, },
"HELLO": "Hallo!", "HELLO": "Hallo!",
"AUDIO": { "AUDIO": {

View File

@ -174,6 +174,8 @@
"SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens", "SCREEN_RENDER_DOWN_HELP": "Helps with aliasing on high-res screens",
"SCROLL_SPEED": "Scroll speed", "SCROLL_SPEED": "Scroll speed",
"SELECT_VARIANT": "Select variant", "SELECT_VARIANT": "Select variant",
"SNAP_ANGLE_DEG": "Window pitch & roll snap angle",
"SNAP_ANGLE_DEG_HELP": "Grabbed windows snap to this angle on pitch and roll axis.",
"ENABLE_WATCH": "Enable watch", "ENABLE_WATCH": "Enable watch",
"SETS_ON_WATCH": "Sets on watch", "SETS_ON_WATCH": "Sets on watch",
"SHOW_WELCOME_SCREEN": "Show welcome screen", "SHOW_WELCOME_SCREEN": "Show welcome screen",

View File

@ -192,7 +192,9 @@
"APPLY_CHANGES_RESTART": "¿Reiniciar WayVR para aplicar los cambios?", "APPLY_CHANGES_RESTART": "¿Reiniciar WayVR para aplicar los cambios?",
"COLOR_PALETTE_HELP": "Haz clic para explorar paletas de colores.\nPara crear paletas personalizadas, ¡visita el sitio web!", "COLOR_PALETTE_HELP": "Haz clic para explorar paletas de colores.\nPara crear paletas personalizadas, ¡visita el sitio web!",
"COLOR_PALETTE_CUSTOM_URL": "¡Añade tu propio tema personalizado!", "COLOR_PALETTE_CUSTOM_URL": "¡Añade tu propio tema personalizado!",
"COLOR_PALETTE_CUSTOM_URL2": "Haz clic para abrir el manual web." "COLOR_PALETTE_CUSTOM_URL2": "Haz clic para abrir el manual web.",
"SNAP_ANGLE_DEG": "Ángulo de ajuste de cabeceo y alabeo de la ventana",
"SNAP_ANGLE_DEG_HELP": "Las ventanas agarradas se ajustan a este ángulo en los ejes de cabeceo y balanceo."
}, },
"HELLO": "¡Hola!", "HELLO": "¡Hola!",
"AUDIO": { "AUDIO": {

View File

@ -213,7 +213,9 @@
"APPLY_CHANGES_RESTART": "Riavviare WayVR per applicare le modifiche?", "APPLY_CHANGES_RESTART": "Riavviare WayVR per applicare le modifiche?",
"COLOR_PALETTE_HELP": "Clicca per sfogliare le palette di colori.\nPer crearne di personalizzate, visita il sito web!", "COLOR_PALETTE_HELP": "Clicca per sfogliare le palette di colori.\nPer crearne di personalizzate, visita il sito web!",
"COLOR_PALETTE_CUSTOM_URL": "Aggiungi il tuo tema personalizzato!", "COLOR_PALETTE_CUSTOM_URL": "Aggiungi il tuo tema personalizzato!",
"COLOR_PALETTE_CUSTOM_URL2": "Clicca per aprire il manuale web." "COLOR_PALETTE_CUSTOM_URL2": "Clicca per aprire il manuale web.",
"SNAP_ANGLE_DEG": "Angolo di scatto beccheggio e rollio finestra",
"SNAP_ANGLE_DEG_HELP": "Le finestre afferrate si allineano a questo angolo sugli assi di beccheggio e rollio."
}, },
"APPLICATION_LAUNCHER": "Lanciatore applicazioni", "APPLICATION_LAUNCHER": "Lanciatore applicazioni",
"APPLICATION_STARTED": "Applicazione avviata", "APPLICATION_STARTED": "Applicazione avviata",

View File

@ -192,7 +192,9 @@
"APPLY_CHANGES_RESTART": "変更を適用するためにWayVRを再起動しますか", "APPLY_CHANGES_RESTART": "変更を適用するためにWayVRを再起動しますか",
"COLOR_PALETTE_HELP": "クリックしてカラーパレットを選択してください。\nカスタムパレットの作成については、ウェブサイトを参照してください", "COLOR_PALETTE_HELP": "クリックしてカラーパレットを選択してください。\nカスタムパレットの作成については、ウェブサイトを参照してください",
"COLOR_PALETTE_CUSTOM_URL": "カスタムテーマを追加しましょう!", "COLOR_PALETTE_CUSTOM_URL": "カスタムテーマを追加しましょう!",
"COLOR_PALETTE_CUSTOM_URL2": "クリックしてウェブマニュアルを開く。" "COLOR_PALETTE_CUSTOM_URL2": "クリックしてウェブマニュアルを開く。",
"SNAP_ANGLE_DEG": "ウィンドウのピッチ・ロールのスナップ角度",
"SNAP_ANGLE_DEG_HELP": "掴んでいるウィンドウが、ピッチ軸とロール軸でこの角度にスナップします。"
}, },
"HELLO": "こんにちは!", "HELLO": "こんにちは!",
"AUDIO": { "AUDIO": {

View File

@ -187,7 +187,9 @@
"APPLY_CHANGES_RESTART": "Zrestartować WayVR, aby zastosować zmiany?", "APPLY_CHANGES_RESTART": "Zrestartować WayVR, aby zastosować zmiany?",
"COLOR_PALETTE_HELP": "Kliknij, aby przeglądać palety kolorów.\nAby stworzyć własne, odwiedź stronę internetową!", "COLOR_PALETTE_HELP": "Kliknij, aby przeglądać palety kolorów.\nAby stworzyć własne, odwiedź stronę internetową!",
"COLOR_PALETTE_CUSTOM_URL": "Dodaj własny motyw!", "COLOR_PALETTE_CUSTOM_URL": "Dodaj własny motyw!",
"COLOR_PALETTE_CUSTOM_URL2": "Kliknij, aby otworzyć instrukcję internetową." "COLOR_PALETTE_CUSTOM_URL2": "Kliknij, aby otworzyć instrukcję internetową.",
"SNAP_ANGLE_DEG": "Kąt przyciągania pochylenia i przechyłu okna",
"SNAP_ANGLE_DEG_HELP": "Chwycone okna przyciągają się do tego kąta na osi pochylenia i obrotu."
}, },
"APPLICATION_LAUNCHER": "Uruchamiacz aplikacji", "APPLICATION_LAUNCHER": "Uruchamiacz aplikacji",
"APPLICATIONS": "Aplikacje", "APPLICATIONS": "Aplikacje",

View File

@ -213,7 +213,9 @@
"APPLY_CHANGES_RESTART": "重启 WayVR 以应用更改吗?", "APPLY_CHANGES_RESTART": "重启 WayVR 以应用更改吗?",
"COLOR_PALETTE_HELP": "点击浏览配色方案。\n如需创建自定义配色请参阅网站", "COLOR_PALETTE_HELP": "点击浏览配色方案。\n如需创建自定义配色请参阅网站",
"COLOR_PALETTE_CUSTOM_URL": "添加自定义主题!", "COLOR_PALETTE_CUSTOM_URL": "添加自定义主题!",
"COLOR_PALETTE_CUSTOM_URL2": "点击打开网页版手册。" "COLOR_PALETTE_CUSTOM_URL2": "点击打开网页版手册。",
"SNAP_ANGLE_DEG": "窗口俯仰与翻滚捕捉角度",
"SNAP_ANGLE_DEG_HELP": "抓取的窗口在俯仰轴和翻滚轴上将对齐到此角度。"
}, },
"APPLICATION_LAUNCHER": "应用启动器", "APPLICATION_LAUNCHER": "应用启动器",
"APPLICATION_STARTED": "应用已启动", "APPLICATION_STARTED": "应用已启动",

View File

@ -320,6 +320,7 @@ pub(crate) enum SettingType {
PointerLerpFactor, PointerLerpFactor,
ScreenRenderDown, ScreenRenderDown,
ScrollSpeed, ScrollSpeed,
SnapAngleDeg,
EnableWatch, EnableWatch,
SetsOnWatch, SetsOnWatch,
SpaceDragMultiplier, SpaceDragMultiplier,
@ -389,6 +390,7 @@ impl SettingType {
Self::LongPressDuration => &mut config.long_press_duration, Self::LongPressDuration => &mut config.long_press_duration,
Self::PointerLerpFactor => &mut config.pointer_lerp_factor, Self::PointerLerpFactor => &mut config.pointer_lerp_factor,
Self::ScrollSpeed => &mut config.scroll_speed, Self::ScrollSpeed => &mut config.scroll_speed,
Self::SnapAngleDeg => &mut config.snap_angle_deg,
Self::SpaceDragMultiplier => &mut config.space_drag_multiplier, Self::SpaceDragMultiplier => &mut config.space_drag_multiplier,
Self::SpaceGravityDamping => &mut config.space_gravity_damping, Self::SpaceGravityDamping => &mut config.space_gravity_damping,
Self::SpaceGravityFlingStrength => &mut config.space_gravity_fling_strength, Self::SpaceGravityFlingStrength => &mut config.space_gravity_fling_strength,
@ -500,6 +502,7 @@ impl SettingType {
Self::ScrollSpeed => Ok("APP_SETTINGS.SCROLL_SPEED"), Self::ScrollSpeed => Ok("APP_SETTINGS.SCROLL_SPEED"),
Self::EnableWatch => Ok("APP_SETTINGS.ENABLE_WATCH"), Self::EnableWatch => Ok("APP_SETTINGS.ENABLE_WATCH"),
Self::SetsOnWatch => Ok("APP_SETTINGS.SETS_ON_WATCH"), Self::SetsOnWatch => Ok("APP_SETTINGS.SETS_ON_WATCH"),
Self::SnapAngleDeg => Ok("APP_SETTINGS.SNAP_ANGLE_DEG"),
Self::SpaceDragMultiplier => Ok("APP_SETTINGS.SPACE_DRAG_MULTIPLIER"), Self::SpaceDragMultiplier => Ok("APP_SETTINGS.SPACE_DRAG_MULTIPLIER"),
Self::SpaceDragUnlocked => Ok("APP_SETTINGS.SPACE_DRAG_UNLOCKED"), Self::SpaceDragUnlocked => Ok("APP_SETTINGS.SPACE_DRAG_UNLOCKED"),
Self::SpaceGravityDamping => Ok("APP_SETTINGS.SPACE_GRAVITY_DAMPING"), Self::SpaceGravityDamping => Ok("APP_SETTINGS.SPACE_GRAVITY_DAMPING"),
@ -536,6 +539,7 @@ impl SettingType {
Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"), Self::KeyboardMiddleClick => Some("APP_SETTINGS.KEYBOARD_MIDDLE_CLICK_HELP"),
Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"), Self::LeftHandedMouse => Some("APP_SETTINGS.LEFT_HANDED_MOUSE_HELP"),
Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"), Self::ScreenRenderDown => Some("APP_SETTINGS.SCREEN_RENDER_DOWN_HELP"),
Self::SnapAngleDeg => Some("APP_SETTINGS.SNAP_ANGLE_DEG_HELP"),
Self::SpaceGravityDamping => Some("APP_SETTINGS.SPACE_GRAVITY_DAMPING_HELP"), Self::SpaceGravityDamping => Some("APP_SETTINGS.SPACE_GRAVITY_DAMPING_HELP"),
Self::SpaceGravityFlingStrength => Some("APP_SETTINGS.SPACE_GRAVITY_FLING_STRENGTH_HELP"), Self::SpaceGravityFlingStrength => Some("APP_SETTINGS.SPACE_GRAVITY_FLING_STRENGTH_HELP"),
Self::SpaceGravityFloorHeight => Some("APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT_HELP"), Self::SpaceGravityFloorHeight => Some("APP_SETTINGS.SPACE_GRAVITY_FLOOR_HEIGHT_HELP"),

View File

@ -10,10 +10,10 @@ use crate::util::popup_manager::PopupHolder;
use crate::{ use crate::{
frontend::FrontendTasks, frontend::FrontendTasks,
tab::settings::{ tab::settings::{
SettingType, SettingsMountParams, SettingsTab,
macros::{options_category, options_checkbox, options_dropdown, options_slider_f32, options_slider_i32}, macros::{options_category, options_checkbox, options_dropdown, options_slider_f32, options_slider_i32},
SettingType, SettingsMountParams, SettingsTab,
}, },
views::{ViewUpdateParams, input_profiles}, views::{input_profiles, ViewUpdateParams},
}; };
#[derive(Clone)] #[derive(Clone)]
@ -104,6 +104,7 @@ impl State {
options_checkbox(par.mp, c, SettingType::InvertScrollDirectionY)?; options_checkbox(par.mp, c, SettingType::InvertScrollDirectionY)?;
options_slider_f32(par.mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1)?; options_slider_f32(par.mp, c, SettingType::ScrollSpeed, 0.1, 5.0, 0.1)?;
options_slider_f32(par.mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1)?; options_slider_f32(par.mp, c, SettingType::LongPressDuration, 0.1, 2.0, 0.1)?;
options_slider_f32(par.mp, c, SettingType::SnapAngleDeg, 0., 45., 5.)?;
if par.feats.openxr { if par.feats.openxr {
options_slider_f32(par.mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1)?; options_slider_f32(par.mp, c, SettingType::PointerLerpFactor, 0.1, 1.0, 0.1)?;

View File

@ -888,7 +888,12 @@ where
app.anchor.translation = app.anchor.translation =
pointer.pose.transform_point3a(grab_data.offset.translation); pointer.pose.transform_point3a(grab_data.offset.translation);
let scale = scalar_scale(&app.anchor); let scale = scalar_scale(&app.anchor);
realign(&mut app.anchor, &app.input_state.hmd, scale); realign(
&mut app.anchor,
&app.input_state.hmd,
scale,
app.session.config.snap_angle_deg,
);
} }
} else { } else {
// single grab resize // single grab resize
@ -912,7 +917,12 @@ where
let scale = scalar_scale(&overlay_state.transform); let scale = scalar_scale(&overlay_state.transform);
overlay_state.transform.translation = overlay_state.transform.translation =
pointer.pose.transform_point3a(grab_data.offset.translation); pointer.pose.transform_point3a(grab_data.offset.translation);
realign(&mut overlay_state.transform, &app.input_state.hmd, scale); realign(
&mut overlay_state.transform,
&app.input_state.hmd,
scale,
app.session.config.snap_angle_deg,
);
} }
overlay.config.pause_movement = true; overlay.config.pause_movement = true;
overlay.config.dirty = true; overlay.config.dirty = true;

View File

@ -187,6 +187,7 @@ pub struct AutoSettings {
pub whisper_model: Arc<str>, pub whisper_model: Arc<str>,
pub default_overlay_scale: f32, pub default_overlay_scale: f32,
pub color_palette: Arc<str>, pub color_palette: Arc<str>,
pub snap_angle_deg: f32,
} }
fn get_settings_path() -> PathBuf { fn get_settings_path() -> PathBuf {
@ -252,6 +253,7 @@ pub fn save_settings(config: &GeneralConfig) -> anyhow::Result<()> {
whisper_model: config.whisper_model.clone(), whisper_model: config.whisper_model.clone(),
default_overlay_scale: config.default_overlay_scale, default_overlay_scale: config.default_overlay_scale,
color_palette: config.color_palette.clone(), color_palette: config.color_palette.clone(),
snap_angle_deg: config.snap_angle_deg,
}; };
let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic let json = serde_json::to_string_pretty(&conf).unwrap(); // want panic

View File

@ -329,7 +329,7 @@ fn tutorial_spawn_effect(app: &mut AppState) -> anyhow::Result<()> {
let hmd = &app.input_state.hmd; let hmd = &app.input_state.hmd;
let pos = hmd.translation + hmd.z_axis * -2.0; let pos = hmd.translation + hmd.z_axis * -2.0;
let mut transform = Affine3A::from_rotation_translation(Quat::IDENTITY, pos.into()); let mut transform = Affine3A::from_rotation_translation(Quat::IDENTITY, pos.into());
realign(&mut transform, hmd, 1.0); realign(&mut transform, hmd, 1.0, app.session.config.snap_angle_deg);
owc.active_state = Some(OverlayWindowState { owc.active_state = Some(OverlayWindowState {
saved_transform: Some(Affine3A::from_translation(vec3(0., -0.1, -0.9))), saved_transform: Some(Affine3A::from_translation(vec3(0., -0.1, -0.9))),

View File

@ -1,4 +1,4 @@
use glam::{Affine3A, Mat3A, Quat, Vec3, Vec3A}; use glam::{Affine3A, EulerRot, Mat3A, Quat, Vec3, Vec3A};
use idmap_derive::IntegerId; use idmap_derive::IntegerId;
use std::{f32::consts::PI, sync::Arc}; use std::{f32::consts::PI, sync::Arc};
use wlx_common::windowing::{OverlayWindowState, Positioning}; use wlx_common::windowing::{OverlayWindowState, Positioning};
@ -192,7 +192,12 @@ impl OverlayWindowConfig {
}; };
if align_to_hmd { if align_to_hmd {
realign(&mut state.transform, &app.input_state.hmd, scale); realign(
&mut state.transform,
&app.input_state.hmd,
scale,
app.session.config.snap_angle_deg,
);
} }
self.dirty = true; self.dirty = true;
@ -250,13 +255,18 @@ impl OverlayWindowConfig {
if align_to_hmd || (state.grabbable && hard_reset) { if align_to_hmd || (state.grabbable && hard_reset) {
let scale = scalar_scale(&cur_transform); let scale = scalar_scale(&cur_transform);
realign(&mut state.transform, &app.input_state.hmd, scale); realign(
&mut state.transform,
&app.input_state.hmd,
scale,
app.session.config.snap_angle_deg,
);
} }
self.dirty = true; self.dirty = true;
} }
} }
pub fn realign(transform: &mut Affine3A, hmd: &Affine3A, scale: f32) { pub fn realign(transform: &mut Affine3A, hmd: &Affine3A, scale: f32, snap_angle: f32) {
let to_hmd = hmd.translation - transform.translation; let to_hmd = hmd.translation - transform.translation;
let up_dir: Vec3A; let up_dir: Vec3A;
@ -289,8 +299,20 @@ pub fn realign(transform: &mut Affine3A, hmd: &Affine3A, scale: f32) {
let col_y = col_z.cross(col_x).normalize(); let col_y = col_z.cross(col_x).normalize();
let col_x = col_x.normalize(); let col_x = col_x.normalize();
let rot = Mat3A::from_quat(Quat::from_axis_angle(Vec3::Y, PI)); let flip = 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; let mut rotation = Mat3A::from_cols(col_x, col_y, col_z) * flip;
if snap_angle > f32::EPSILON {
let snap_step = snap_angle.to_radians();
let (yaw, pitch, roll) = rotation.to_euler(EulerRot::YXZ);
let pitch = (pitch / snap_step).round() * snap_step;
let roll = (roll / snap_step).round() * snap_step;
rotation = Mat3A::from_euler(EulerRot::YXZ, yaw, pitch, roll);
}
transform.matrix3 = rotation.mul_scalar(scale);
} }
pub fn scalar_scale(a: &Affine3A) -> f32 { pub fn scalar_scale(a: &Affine3A) -> f32 {

View File

@ -475,4 +475,7 @@ pub struct GeneralConfig {
#[serde(default = "def_default")] #[serde(default = "def_default")]
pub color_palette: Arc<str>, pub color_palette: Arc<str>,
#[serde(default)]
pub snap_angle_deg: f32,
} }