minor cursor handling refactor improvemnts
This commit is contained in:
@@ -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<AppState, String> {
|
||||
Ok(app_state::get_snapshot())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_neko_positions() -> Result<neko_positions::NekoPositionsDto, String> {
|
||||
Ok(neko_positions::get_snapshot())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
|
||||
|
||||
@@ -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<tauri::AppHandle<tauri::Wry>> = 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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
#[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")]
|
||||
|
||||
@@ -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<Arc<RwLock<AppState>>> =
|
||||
@@ -21,6 +21,8 @@ pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
|
||||
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) {
|
||||
|
||||
@@ -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::<CursorPositions>(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)");
|
||||
});
|
||||
|
||||
@@ -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<String, CursorPositions>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct FriendCursorProjection {
|
||||
active_dolls: HashMap<String, bool>,
|
||||
positions: HashMap<String, CursorPositions>,
|
||||
}
|
||||
|
||||
static FRIEND_CURSOR_PROJECTION: LazyLock<Arc<RwLock<FriendCursorProjection>>> =
|
||||
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<String, CursorPositions>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
307
src-tauri/src/services/neko_positions.rs
Normal file
307
src-tauri/src/services/neko_positions.rs
Normal file
@@ -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<String, NekoPositionDto>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct NekoPositionsProjection {
|
||||
self_cursor: Option<CursorPositions>,
|
||||
friend_cursors: HashMap<String, CursorPositions>,
|
||||
friend_active_dolls: HashMap<String, bool>,
|
||||
}
|
||||
|
||||
static NEKO_POSITIONS: LazyLock<Arc<RwLock<NekoPositionsProjection>>> =
|
||||
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<String> {
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user