From 53248243e3b6a9cf4fddc782b102d5d3acf37a1f Mon Sep 17 00:00:00 2001 From: Wind-Explorer Date: Wed, 25 Mar 2026 02:14:55 +0800 Subject: [PATCH] minor cursor handling refactor improvemnts --- src-tauri/src/commands/app_state.rs | 7 + src-tauri/src/lib.rs | 15 +- src-tauri/src/services/app_data/refresh.rs | 4 +- src-tauri/src/services/app_events.rs | 13 +- src-tauri/src/services/app_state.rs | 4 +- src-tauri/src/services/cursor.rs | 21 +- .../src/services/friends/cursor_positions.rs | 145 --------- src-tauri/src/services/friends/mod.rs | 16 +- src-tauri/src/services/mod.rs | 1 + src-tauri/src/services/neko_positions.rs | 307 ++++++++++++++++++ src-tauri/src/services/ws/friend.rs | 15 +- src/events/cursor.ts | 17 - src/events/friend-cursor.ts | 32 -- src/events/neko-positions.ts | 16 + src/lib/bindings.ts | 15 +- src/routes/+layout.svelte | 13 +- src/routes/scene/+page.svelte | 64 ++-- src/routes/scene/components/debug-bar.svelte | 40 ++- 18 files changed, 443 insertions(+), 302 deletions(-) delete mode 100644 src-tauri/src/services/friends/cursor_positions.rs create mode 100644 src-tauri/src/services/neko_positions.rs delete mode 100644 src/events/cursor.ts delete mode 100644 src/events/friend-cursor.ts create mode 100644 src/events/neko-positions.ts diff --git a/src-tauri/src/commands/app_state.rs b/src-tauri/src/commands/app_state.rs index 02b20de..086e23a 100644 --- a/src-tauri/src/commands/app_state.rs +++ b/src-tauri/src/commands/app_state.rs @@ -5,6 +5,7 @@ use crate::{ app_data::{init_app_data_scoped, AppDataRefreshScope}, app_state, friends, + neko_positions, presence_modules::models::ModuleMetadata, sprite, }, @@ -53,6 +54,12 @@ pub fn get_app_state() -> Result { Ok(app_state::get_snapshot()) } +#[tauri::command] +#[specta::specta] +pub fn get_neko_positions() -> Result { + Ok(neko_positions::get_snapshot()) +} + #[tauri::command] #[specta::specta] pub fn set_scene_setup_nekos_position(nekos_position: Option) { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 18518e8..55d732b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,7 +4,7 @@ use crate::services::{ }; use commands::app::{quit_app, restart_app, retry_connection}; use commands::app_state::{ - get_active_doll_sprite_base64, get_app_data, get_app_state, + get_active_doll_sprite_base64, get_app_data, get_app_state, get_neko_positions, get_friend_active_doll_sprites_base64, get_modules, refresh_app_data, set_scene_setup_nekos_opacity, set_scene_setup_nekos_position, set_scene_setup_nekos_scale, }; @@ -26,11 +26,10 @@ use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, E use crate::services::app_events::{ ActiveDollSpriteChanged, AppDataRefreshed, AppStateChanged, AuthFlowUpdated, CreateDoll, - CursorMoved, EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated, - FriendCursorPositionsUpdated, FriendDisconnected, FriendRequestAccepted, - FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, - InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged, - SetInteractionOverlay, Unfriended, UserStatusChanged, + EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendDisconnected, + FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged, + InteractionDeliveryFailed, InteractionReceived, NekoPositionsUpdated, + SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged, }; static APP_HANDLE: std::sync::OnceLock> = std::sync::OnceLock::new(); @@ -68,6 +67,7 @@ pub fn run() { .commands(collect_commands![ get_app_data, get_app_state, + get_neko_positions, get_active_doll_sprite_base64, get_friend_active_doll_sprites_base64, refresh_app_data, @@ -108,16 +108,15 @@ pub fn run() { set_scene_setup_nekos_scale ]) .events(collect_events![ - CursorMoved, SceneInteractiveChanged, AppDataRefreshed, AppStateChanged, + NekoPositionsUpdated, ActiveDollSpriteChanged, SetInteractionOverlay, EditDoll, CreateDoll, UserStatusChanged, - FriendCursorPositionsUpdated, FriendDisconnected, FriendActiveDollChanged, FriendActiveDollSpritesUpdated, diff --git a/src-tauri/src/services/app_data/refresh.rs b/src-tauri/src/services/app_data/refresh.rs index 72a0bb3..d42976f 100644 --- a/src-tauri/src/services/app_data/refresh.rs +++ b/src-tauri/src/services/app_data/refresh.rs @@ -11,7 +11,7 @@ use crate::{ remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote}, services::{ app_events::{ActiveDollSpriteChanged, AppDataRefreshed}, - friends, sprite, + friends, neko_positions, sprite, }, state::FDOLL, }; @@ -51,6 +51,8 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) { Ok(user) => { let mut guard = lock_w!(FDOLL); guard.user_data.user = Some(user); + drop(guard); + neko_positions::sync_from_app_data(); } Err(error) => { warn!("Failed to fetch user profile: {}", error); diff --git a/src-tauri/src/services/app_events.rs b/src-tauri/src/services/app_events.rs index 1f71388..c96d9d0 100644 --- a/src-tauri/src/services/app_events.rs +++ b/src-tauri/src/services/app_events.rs @@ -13,10 +13,7 @@ use crate::{ }, interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto}, }, - services::{ - cursor::CursorPositions, - friends::{FriendActiveDollSpritesDto, FriendCursorPositionsDto}, - }, + services::{friends::FriendActiveDollSpritesDto, neko_positions::NekoPositionsDto}, }; #[derive(Debug, Clone, Serialize, Deserialize, Type)] @@ -36,10 +33,6 @@ pub struct AuthFlowUpdatedPayload { pub message: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] -#[tauri_specta(event_name = "cursor-position")] -pub struct CursorMoved(pub CursorPositions); - #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] #[tauri_specta(event_name = "scene-interactive")] pub struct SceneInteractiveChanged(pub bool); @@ -73,8 +66,8 @@ pub struct CreateDoll; pub struct UserStatusChanged(pub UserStatusPayload); #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] -#[tauri_specta(event_name = "friend-cursor-positions")] -pub struct FriendCursorPositionsUpdated(pub FriendCursorPositionsDto); +#[tauri_specta(event_name = "neko-positions")] +pub struct NekoPositionsUpdated(pub NekoPositionsDto); #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] #[tauri_specta(event_name = "friend-disconnected")] diff --git a/src-tauri/src/services/app_state.rs b/src-tauri/src/services/app_state.rs index 3feaff1..bd52c57 100644 --- a/src-tauri/src/services/app_state.rs +++ b/src-tauri/src/services/app_state.rs @@ -6,7 +6,7 @@ use tracing::warn; use crate::{ get_app_handle, models::app_state::{AppState, NekoPosition}, - services::app_events::AppStateChanged, + services::{app_events::AppStateChanged, neko_positions}, }; static APP_STATE: LazyLock>> = @@ -21,6 +21,8 @@ pub fn set_scene_setup_nekos_position(nekos_position: Option) { let mut guard = APP_STATE.write().expect("app state lock poisoned"); guard.scene_setup.nekos_position = nekos_position; emit_snapshot(&guard); + drop(guard); + neko_positions::refresh_from_scene_setup(); } pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) { diff --git a/src-tauri/src/services/cursor.rs b/src-tauri/src/services/cursor.rs index d596c11..efeadb6 100644 --- a/src-tauri/src/services/cursor.rs +++ b/src-tauri/src/services/cursor.rs @@ -7,8 +7,11 @@ use std::time::Duration; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; -use crate::{get_app_handle, lock_r, services::app_events::CursorMoved, state::FDOLL}; -use tauri_specta::Event as _; +use crate::{ + lock_r, + services::{neko_positions, ws::report_cursor_data}, + state::FDOLL, +}; #[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] @@ -59,8 +62,7 @@ pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition { } } -/// Initialize cursor tracking. Broadcasts cursor -/// position changes via `cursor-position` event. +/// Initialize cursor tracking. pub async fn init_cursor_tracking() { info!("start_cursor_tracking called"); @@ -88,22 +90,19 @@ async fn init_cursor_tracking_i() -> Result<(), String> { let (tx, mut rx) = mpsc::channel::(100); // Spawn the consumer task - // This task handles WebSocket reporting and local broadcasting. + // This task handles WebSocket reporting and local position projection updates. // It runs independently of the device event loop. tauri::async_runtime::spawn(async move { info!("Cursor event consumer started"); - let app_handle = get_app_handle(); while let Some(positions) = rx.recv().await { let mapped_for_ws = positions.mapped.clone(); // 1. WebSocket reporting - crate::services::ws::report_cursor_data(mapped_for_ws).await; + report_cursor_data(mapped_for_ws).await; - // 2. Broadcast to local windows - if let Err(e) = CursorMoved(positions).emit(app_handle) { - error!("Failed to emit cursor position event: {:?}", e); - } + // 2. Update unified neko positions projection + neko_positions::update_self_cursor(positions); } warn!("Cursor event consumer stopped (channel closed)"); }); diff --git a/src-tauri/src/services/friends/cursor_positions.rs b/src-tauri/src/services/friends/cursor_positions.rs deleted file mode 100644 index a780b6f..0000000 --- a/src-tauri/src/services/friends/cursor_positions.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::{ - collections::HashMap, - sync::{Arc, LazyLock, RwLock}, -}; - -use serde::{Deserialize, Serialize}; -use specta::Type; -use tauri_specta::Event as _; - -use crate::{ - get_app_handle, lock_r, - services::{app_events::FriendCursorPositionsUpdated, cursor::CursorPositions}, - state::FDOLL, -}; - -#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)] -#[serde(transparent)] -pub struct FriendCursorPositionsDto(pub HashMap); - -#[derive(Default)] -struct FriendCursorProjection { - active_dolls: HashMap, - positions: HashMap, -} - -static FRIEND_CURSOR_PROJECTION: LazyLock>> = - LazyLock::new(|| Arc::new(RwLock::new(FriendCursorProjection::default()))); - -pub fn sync_from_app_data() { - let friends = { - let guard = lock_r!(FDOLL); - guard.user_data.friends.clone().unwrap_or_default() - }; - - let mut projection = FRIEND_CURSOR_PROJECTION - .write() - .expect("friend cursor projection lock poisoned"); - - projection.active_dolls = friends - .into_iter() - .filter_map(|friendship| { - friendship.friend.map(|friend| { - let has_active_doll = friend.active_doll.is_some(); - (friend.id, has_active_doll) - }) - }) - .collect(); - - let active_dolls = projection.active_dolls.clone(); - projection - .positions - .retain(|user_id, _| active_dolls.get(user_id) == Some(&true)); - - emit_snapshot(&projection.positions); -} - -pub fn clear() { - let mut projection = FRIEND_CURSOR_PROJECTION - .write() - .expect("friend cursor projection lock poisoned"); - - projection.active_dolls.clear(); - projection.positions.clear(); - - emit_snapshot(&projection.positions); -} - -pub fn update_position(user_id: String, position: CursorPositions) { - let mut projection = FRIEND_CURSOR_PROJECTION - .write() - .expect("friend cursor projection lock poisoned"); - - if !has_active_doll(&mut projection, &user_id) { - if projection.positions.remove(&user_id).is_some() { - emit_snapshot(&projection.positions); - } - return; - } - - projection.positions.insert(user_id, position); - emit_snapshot(&projection.positions); -} - -pub fn remove_friend(user_id: &str) { - let mut projection = FRIEND_CURSOR_PROJECTION - .write() - .expect("friend cursor projection lock poisoned"); - - let removed_active_doll = projection.active_dolls.remove(user_id).is_some(); - let removed_position = projection.positions.remove(user_id).is_some(); - - if removed_active_doll || removed_position { - emit_snapshot(&projection.positions); - } -} - -pub fn set_active_doll(user_id: &str, has_active_doll: bool) { - let mut projection = FRIEND_CURSOR_PROJECTION - .write() - .expect("friend cursor projection lock poisoned"); - - projection - .active_dolls - .insert(user_id.to_string(), has_active_doll); - - if !has_active_doll && projection.positions.remove(user_id).is_some() { - emit_snapshot(&projection.positions); - } -} - -fn has_active_doll(projection: &mut FriendCursorProjection, user_id: &str) -> bool { - if let Some(has_active_doll) = projection.active_dolls.get(user_id) { - return *has_active_doll; - } - - let has_active_doll = { - let guard = lock_r!(FDOLL); - guard - .user_data - .friends - .as_ref() - .and_then(|friends| { - friends.iter().find_map(|friendship| { - let friend = friendship.friend.as_ref()?; - (friend.id == user_id).then_some(friend) - }) - }) - .and_then(|friend| friend.active_doll.as_ref()) - .is_some() - }; - - projection - .active_dolls - .insert(user_id.to_string(), has_active_doll); - - has_active_doll -} - -fn emit_snapshot(positions: &HashMap) { - let payload = FriendCursorPositionsDto(positions.clone()); - - if let Err(err) = FriendCursorPositionsUpdated(payload).emit(get_app_handle()) { - tracing::warn!("Failed to emit friend cursor positions update: {}", err); - } -} diff --git a/src-tauri/src/services/friends/mod.rs b/src-tauri/src/services/friends/mod.rs index 6903521..6a5629e 100644 --- a/src-tauri/src/services/friends/mod.rs +++ b/src-tauri/src/services/friends/mod.rs @@ -1,33 +1,27 @@ mod active_doll_sprites; -mod cursor_positions; -use crate::{models::dolls::DollDto, services::cursor::CursorPositions}; +use crate::{models::dolls::DollDto, services::neko_positions}; pub use active_doll_sprites::FriendActiveDollSpritesDto; -pub use cursor_positions::FriendCursorPositionsDto; pub fn sync_from_app_data() { active_doll_sprites::sync_from_app_data(); - cursor_positions::sync_from_app_data(); + neko_positions::sync_from_app_data(); } pub fn clear() { active_doll_sprites::clear(); - cursor_positions::clear(); + neko_positions::clear(); } pub fn remove_friend(user_id: &str) { active_doll_sprites::remove_friend(user_id); - cursor_positions::remove_friend(user_id); + neko_positions::remove_friend(user_id); } pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) { active_doll_sprites::set_active_doll(user_id, doll); - cursor_positions::set_active_doll(user_id, doll.is_some()); -} - -pub fn update_cursor_position(user_id: String, position: CursorPositions) { - cursor_positions::update_position(user_id, position); + neko_positions::set_friend_active_doll(user_id, doll.is_some()); } pub fn sync_active_doll_sprites_from_app_data() { diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index fdde507..7144cb6 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -12,6 +12,7 @@ pub mod friends; pub mod health_manager; pub mod health_monitor; pub mod interaction; +pub mod neko_positions; pub mod petpet; pub mod presence_modules; pub mod scene; diff --git a/src-tauri/src/services/neko_positions.rs b/src-tauri/src/services/neko_positions.rs new file mode 100644 index 0000000..c5fbb9b --- /dev/null +++ b/src-tauri/src/services/neko_positions.rs @@ -0,0 +1,307 @@ +use std::{ + collections::HashMap, + sync::{Arc, LazyLock, RwLock}, +}; + +use serde::{Deserialize, Serialize}; +use specta::Type; +use tauri_specta::Event as _; + +use crate::{ + get_app_handle, lock_r, + models::app_state::NekoPosition, + services::{ + app_events::NekoPositionsUpdated, + app_state, + cursor::{CursorPosition, CursorPositions}, + }, + state::FDOLL, +}; + +#[derive(Clone, Debug, Serialize, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct NekoPositionDto { + pub user_id: String, + pub is_self: bool, + pub cursor: CursorPositions, + pub target: CursorPosition, + pub override_applied: bool, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)] +#[serde(transparent)] +pub struct NekoPositionsDto(pub HashMap); + +#[derive(Default)] +struct NekoPositionsProjection { + self_cursor: Option, + friend_cursors: HashMap, + friend_active_dolls: HashMap, +} + +static NEKO_POSITIONS: LazyLock>> = + LazyLock::new(|| Arc::new(RwLock::new(NekoPositionsProjection::default()))); + +pub fn sync_from_app_data() { + let friends = { + let guard = lock_r!(FDOLL); + guard.user_data.friends.clone().unwrap_or_default() + }; + + let mut projection = NEKO_POSITIONS + .write() + .expect("neko positions projection lock poisoned"); + + projection.friend_active_dolls = friends + .into_iter() + .filter_map(|friendship| { + friendship.friend.map(|friend| { + let has_active_doll = friend.active_doll.is_some(); + (friend.id, has_active_doll) + }) + }) + .collect(); + + let active_dolls = projection.friend_active_dolls.clone(); + projection + .friend_cursors + .retain(|user_id, _| active_dolls.get(user_id) == Some(&true)); + + emit_snapshot(&projection); +} + +pub fn clear() { + let mut projection = NEKO_POSITIONS + .write() + .expect("neko positions projection lock poisoned"); + + projection.self_cursor = None; + projection.friend_cursors.clear(); + projection.friend_active_dolls.clear(); + + emit_snapshot(&projection); +} + +pub fn update_self_cursor(position: CursorPositions) { + let mut projection = NEKO_POSITIONS + .write() + .expect("neko positions projection lock poisoned"); + + projection.self_cursor = Some(position); + emit_snapshot(&projection); +} + +pub fn update_friend_cursor(user_id: String, position: CursorPositions) { + let mut projection = NEKO_POSITIONS + .write() + .expect("neko positions projection lock poisoned"); + + if !has_friend_active_doll(&mut projection, &user_id) { + if projection.friend_cursors.remove(&user_id).is_some() { + emit_snapshot(&projection); + } + return; + } + + projection.friend_cursors.insert(user_id, position); + emit_snapshot(&projection); +} + +pub fn remove_friend(user_id: &str) { + let mut projection = NEKO_POSITIONS + .write() + .expect("neko positions projection lock poisoned"); + + let removed_active_doll = projection.friend_active_dolls.remove(user_id).is_some(); + let removed_position = projection.friend_cursors.remove(user_id).is_some(); + + if removed_active_doll || removed_position { + emit_snapshot(&projection); + } +} + +pub fn set_friend_active_doll(user_id: &str, has_active_doll: bool) { + let mut projection = NEKO_POSITIONS + .write() + .expect("neko positions projection lock poisoned"); + + projection + .friend_active_dolls + .insert(user_id.to_string(), has_active_doll); + + if !has_active_doll && projection.friend_cursors.remove(user_id).is_some() { + emit_snapshot(&projection); + } +} + +pub fn refresh_from_scene_setup() { + let projection = NEKO_POSITIONS + .read() + .expect("neko positions projection lock poisoned"); + emit_snapshot(&projection); +} + +pub fn get_snapshot() -> NekoPositionsDto { + let projection = NEKO_POSITIONS + .read() + .expect("neko positions projection lock poisoned"); + build_snapshot(&projection) +} + +fn has_friend_active_doll(projection: &mut NekoPositionsProjection, user_id: &str) -> bool { + if let Some(has_active_doll) = projection.friend_active_dolls.get(user_id) { + return *has_active_doll; + } + + let has_active_doll = { + let guard = lock_r!(FDOLL); + guard + .user_data + .friends + .as_ref() + .and_then(|friends| { + friends.iter().find_map(|friendship| { + let friend = friendship.friend.as_ref()?; + (friend.id == user_id).then_some(friend) + }) + }) + .and_then(|friend| friend.active_doll.as_ref()) + .is_some() + }; + + projection + .friend_active_dolls + .insert(user_id.to_string(), has_active_doll); + + has_active_doll +} + +fn has_self_active_doll() -> bool { + let guard = lock_r!(FDOLL); + guard + .user_data + .user + .as_ref() + .and_then(|user| user.active_doll_id.as_ref()) + .is_some() +} + +fn get_self_user_id() -> Option { + let guard = lock_r!(FDOLL); + guard.user_data.user.as_ref().map(|user| user.id.clone()) +} + +fn get_display_size() -> (f64, f64) { + let guard = lock_r!(FDOLL); + ( + guard.user_data.scene.display.screen_width as f64, + guard.user_data.scene.display.screen_height as f64, + ) +} + +fn emit_snapshot(projection: &NekoPositionsProjection) { + let payload = build_snapshot(projection); + + if let Err(err) = NekoPositionsUpdated(payload).emit(get_app_handle()) { + tracing::warn!("Failed to emit neko positions update: {}", err); + } +} + +fn build_snapshot(projection: &NekoPositionsProjection) -> NekoPositionsDto { + let mut entries: Vec<(String, bool, CursorPositions)> = Vec::new(); + + if has_self_active_doll() { + if let (Some(self_user_id), Some(self_cursor)) = + (get_self_user_id(), projection.self_cursor.clone()) + { + entries.push((self_user_id, true, self_cursor)); + } + } + + for (user_id, cursor) in &projection.friend_cursors { + if projection.friend_active_dolls.get(user_id) == Some(&true) { + entries.push((user_id.clone(), false, cursor.clone())); + } + } + + entries.sort_by(|a, b| a.0.cmp(&b.0)); + + let app_state = app_state::get_snapshot(); + let override_anchor = app_state.scene_setup.nekos_position; + let (screen_width, screen_height) = get_display_size(); + + let total = entries.len(); + + NekoPositionsDto( + entries + .into_iter() + .enumerate() + .map(|(index, (user_id, is_self, cursor))| { + let (target, override_applied) = match &override_anchor { + Some(anchor) => ( + get_cluster_target( + anchor.clone(), + index, + total, + screen_width, + screen_height, + ), + true, + ), + None => (cursor.raw.clone(), false), + }; + + ( + user_id.clone(), + NekoPositionDto { + user_id, + is_self, + cursor, + target, + override_applied, + }, + ) + }) + .collect(), + ) +} + +fn get_cluster_target( + anchor: NekoPosition, + index: usize, + count: usize, + screen_width: f64, + screen_height: f64, +) -> CursorPosition { + let spacing = 36.0; + let margin = 28.0; + + let columns = (count as f64).sqrt().ceil().max(1.0) as usize; + let rows = count.div_ceil(columns).max(1); + let col = index % columns; + let row = index / columns; + + let block_width = (columns.saturating_sub(1)) as f64 * spacing; + let block_height = (rows.saturating_sub(1)) as f64 * spacing; + + let start_x = match anchor { + NekoPosition::TopLeft | NekoPosition::Left | NekoPosition::BottomLeft => margin, + NekoPosition::Top | NekoPosition::Bottom => (screen_width - block_width) / 2.0, + NekoPosition::TopRight | NekoPosition::Right | NekoPosition::BottomRight => { + screen_width - margin - block_width + } + }; + + let start_y = match anchor { + NekoPosition::TopLeft | NekoPosition::Top | NekoPosition::TopRight => margin, + NekoPosition::Left | NekoPosition::Right => (screen_height - block_height) / 2.0, + NekoPosition::BottomLeft | NekoPosition::Bottom | NekoPosition::BottomRight => { + screen_height - margin - block_height + } + }; + + CursorPosition { + x: (start_x + col as f64 * spacing).clamp(0.0, screen_width), + y: (start_y + row as f64 * spacing).clamp(0.0, screen_height), + } +} diff --git a/src-tauri/src/services/ws/friend.rs b/src-tauri/src/services/ws/friend.rs index ce7b812..9a539a4 100644 --- a/src-tauri/src/services/ws/friend.rs +++ b/src-tauri/src/services/ws/friend.rs @@ -13,7 +13,7 @@ use crate::services::app_events::{ }; use crate::services::{ cursor::{normalized_to_absolute, CursorPositions}, - friends, + friends, neko_positions, }; use super::{emitter, refresh, types::IncomingFriendCursorPayload, utils}; @@ -62,13 +62,12 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) { let mapped_pos = &friend_data.position; let raw_pos = normalized_to_absolute(mapped_pos); - friends::update_cursor_position( - friend_data.user_id, - CursorPositions { - raw: raw_pos, - mapped: mapped_pos.clone(), - }, - ); + let position = CursorPositions { + raw: raw_pos, + mapped: mapped_pos.clone(), + }; + + neko_positions::update_friend_cursor(friend_data.user_id, position); } } diff --git a/src/events/cursor.ts b/src/events/cursor.ts deleted file mode 100644 index 04a21ba..0000000 --- a/src/events/cursor.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { writable } from "svelte/store"; -import { events, type CursorPositions } from "$lib/bindings"; -import { createEventSource } from "./listener-utils"; - -export const cursorPositionOnScreen = writable({ - raw: { x: 0, y: 0 }, - mapped: { x: 0, y: 0 }, -}); - -export const { start: startCursorTracking, stop: stopCursorTracking } = - createEventSource(async (addEventListener) => { - addEventListener( - await events.cursorMoved.listen((event) => { - cursorPositionOnScreen.set(event.payload); - }), - ); - }); diff --git a/src/events/friend-cursor.ts b/src/events/friend-cursor.ts deleted file mode 100644 index f98009c..0000000 --- a/src/events/friend-cursor.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { writable } from "svelte/store"; -import { events, type CursorPositions } from "$lib/bindings"; -import { createEventSource } from "./listener-utils"; - -export const friendsCursorPositions = writable>( - {}, -); - -// Here for now. Will extract into shared -// util when there's more similar cases. -function toCursorPositionsRecord( - payload: Partial>, -): Record { - return Object.fromEntries( - Object.entries(payload).filter( - (entry): entry is [string, CursorPositions] => { - return entry[1] !== undefined; - }, - ), - ); -} - -export const { - start: startFriendCursorTracking, - stop: stopFriendCursorTracking, -} = createEventSource(async (addEventListener) => { - addEventListener( - await events.friendCursorPositionsUpdated.listen((event) => { - friendsCursorPositions.set(toCursorPositionsRecord(event.payload)); - }), - ); -}); diff --git a/src/events/neko-positions.ts b/src/events/neko-positions.ts new file mode 100644 index 0000000..5215fe3 --- /dev/null +++ b/src/events/neko-positions.ts @@ -0,0 +1,16 @@ +import { writable } from "svelte/store"; +import { commands, events, type NekoPositionsDto } from "$lib/bindings"; +import { createEventSource } from "./listener-utils"; + +export const nekoPositions = writable({}); + +export const { start: startNekoPositions, stop: stopNekoPositions } = + createEventSource(async (addEventListener) => { + nekoPositions.set(await commands.getNekoPositions()); + + addEventListener( + await events.nekoPositionsUpdated.listen((event) => { + nekoPositions.set(event.payload); + }), + ); + }); diff --git a/src/lib/bindings.ts b/src/lib/bindings.ts index c4f31cd..e60e569 100644 --- a/src/lib/bindings.ts +++ b/src/lib/bindings.ts @@ -11,6 +11,9 @@ async getAppData() : Promise { async getAppState() : Promise { return await TAURI_INVOKE("get_app_state"); }, +async getNekoPositions() : Promise { + return await TAURI_INVOKE("get_neko_positions"); +}, async getActiveDollSpriteBase64() : Promise { return await TAURI_INVOKE("get_active_doll_sprite_base64"); }, @@ -142,11 +145,9 @@ appDataRefreshed: AppDataRefreshed, appStateChanged: AppStateChanged, authFlowUpdated: AuthFlowUpdated, createDoll: CreateDoll, -cursorMoved: CursorMoved, editDoll: EditDoll, friendActiveDollChanged: FriendActiveDollChanged, friendActiveDollSpritesUpdated: FriendActiveDollSpritesUpdated, -friendCursorPositionsUpdated: FriendCursorPositionsUpdated, friendDisconnected: FriendDisconnected, friendRequestAccepted: FriendRequestAccepted, friendRequestDenied: FriendRequestDenied, @@ -154,6 +155,7 @@ friendRequestReceived: FriendRequestReceived, friendUserStatusChanged: FriendUserStatusChanged, interactionDeliveryFailed: InteractionDeliveryFailed, interactionReceived: InteractionReceived, +nekoPositionsUpdated: NekoPositionsUpdated, sceneInteractiveChanged: SceneInteractiveChanged, setInteractionOverlay: SetInteractionOverlay, unfriended: Unfriended, @@ -164,11 +166,9 @@ appDataRefreshed: "app-data-refreshed", appStateChanged: "app-state-changed", authFlowUpdated: "auth-flow-updated", createDoll: "create-doll", -cursorMoved: "cursor-moved", editDoll: "edit-doll", friendActiveDollChanged: "friend-active-doll-changed", friendActiveDollSpritesUpdated: "friend-active-doll-sprites-updated", -friendCursorPositionsUpdated: "friend-cursor-positions-updated", friendDisconnected: "friend-disconnected", friendRequestAccepted: "friend-request-accepted", friendRequestDenied: "friend-request-denied", @@ -176,6 +176,7 @@ friendRequestReceived: "friend-request-received", friendUserStatusChanged: "friend-user-status-changed", interactionDeliveryFailed: "interaction-delivery-failed", interactionReceived: "interaction-received", +nekoPositionsUpdated: "neko-positions-updated", sceneInteractiveChanged: "scene-interactive-changed", setInteractionOverlay: "set-interaction-overlay", unfriended: "unfriended", @@ -201,7 +202,6 @@ export type AuthFlowUpdated = AuthFlowUpdatedPayload export type AuthFlowUpdatedPayload = { provider: string; status: AuthFlowStatus; message: string | null } export type CreateDoll = null export type CreateDollDto = { name: string; configuration: DollConfigurationDto | null } -export type CursorMoved = CursorPositions export type CursorPosition = { x: number; y: number } export type CursorPositions = { raw: CursorPosition; mapped: CursorPosition } export type DisplayData = { screen_width: number; screen_height: number; monitor_scale_factor: number } @@ -213,8 +213,6 @@ export type FriendActiveDollChanged = FriendActiveDollChangedPayload export type FriendActiveDollChangedPayload = { friendId: string; doll: DollDto | null } export type FriendActiveDollSpritesDto = Partial<{ [key in string]: string }> export type FriendActiveDollSpritesUpdated = FriendActiveDollSpritesDto -export type FriendCursorPositionsDto = Partial<{ [key in string]: CursorPositions }> -export type FriendCursorPositionsUpdated = FriendCursorPositionsDto export type FriendDisconnected = FriendDisconnectedPayload export type FriendDisconnectedPayload = { userId: string } export type FriendRequestAccepted = FriendRequestAcceptedPayload @@ -234,6 +232,9 @@ export type InteractionReceived = InteractionPayloadDto export type KeyboardAccelerator = { modifiers?: AcceleratorModifier[]; key?: AcceleratorKey | null } export type ModuleMetadata = { id: string; name: string; version: string; description: string | null } export type NekoPosition = "top-left" | "top" | "top-right" | "left" | "right" | "bottom-left" | "bottom" | "bottom-right" +export type NekoPositionDto = { userId: string; isSelf: boolean; cursor: CursorPositions; target: CursorPosition; overrideApplied: boolean } +export type NekoPositionsDto = Partial<{ [key in string]: NekoPositionDto }> +export type NekoPositionsUpdated = NekoPositionsDto export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null } export type SceneData = { display: DisplayData; grid_size: number } export type SceneInteractiveChanged = boolean diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 93f0efc..2fe4491 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,11 +1,10 @@
@@ -42,43 +47,38 @@ await commands.setSceneInteractive(false, true); }}>  - {#if $appData?.user?.activeDollId} - - {/if} - {#each Object.entries($friendsCursorPositions) as [friendId, position] (friendId)} - {#if $friendActiveDollSpriteUrls[friendId]} - {@const friend = getFriend(friendId)} + {#each nekoEntries as [userId, position] (userId)} + {@const spriteUrl = position.isSelf + ? $activeDollSpriteUrl + : $friendActiveDollSpriteUrls[userId]} + {#if spriteUrl} + {@const friend = position.isSelf ? undefined : getFriend(userId)} - - - + {#if !position.isSelf && friend} + + + + {/if} {/if} {/each} {#if debugMode}
- +
{/if}
diff --git a/src/routes/scene/components/debug-bar.svelte b/src/routes/scene/components/debug-bar.svelte index 9fc69f2..f5b9ae1 100644 --- a/src/routes/scene/components/debug-bar.svelte +++ b/src/routes/scene/components/debug-bar.svelte @@ -1,5 +1,10 @@