resolve friends' grid to absolute

This commit is contained in:
2025-12-16 22:27:55 +08:00
parent 8185b2c9a1
commit edeb1f4d78
4 changed files with 70 additions and 33 deletions

View File

@@ -1,6 +1,6 @@
use device_query::{DeviceEvents, DeviceEventsHandler}; use device_query::{DeviceEvents, DeviceEventsHandler};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::Serialize; use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@@ -10,7 +10,7 @@ use ts_rs::TS;
use crate::{get_app_handle, lock_r, state::FDOLL}; use crate::{get_app_handle, lock_r, state::FDOLL};
#[derive(Clone, Serialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct CursorPosition { pub struct CursorPosition {
@@ -18,7 +18,7 @@ pub struct CursorPosition {
pub y: i32, pub y: i32,
} }
#[derive(Clone, Serialize, TS)] #[derive(Debug, Clone, Serialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export)] #[ts(export)]
pub struct CursorPositions { pub struct CursorPositions {

View File

@@ -5,12 +5,28 @@ use tracing::{error, info};
use crate::{ use crate::{
get_app_handle, lock_r, lock_w, models::app_config::AppConfig, get_app_handle, lock_r, lock_w, models::app_config::AppConfig,
services::cursor::CursorPosition, state::FDOLL, services::cursor::{grid_to_absolute_position, CursorPosition, CursorPositions},
state::FDOLL,
}; };
use serde::{Deserialize, Serialize};
#[allow(non_camel_case_types)] // pretend to be a const like in js #[allow(non_camel_case_types)] // pretend to be a const like in js
pub struct WS_EVENT; pub struct WS_EVENT;
#[derive(Debug, Deserialize)]
struct IncomingFriendCursorPayload {
#[serde(rename = "userId")]
user_id: String,
position: CursorPosition,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct OutgoingFriendCursorPayload {
user_id: String,
position: CursorPositions,
}
impl WS_EVENT { impl WS_EVENT {
pub const CURSOR_REPORT_POSITION: &str = "cursor-report-position"; pub const CURSOR_REPORT_POSITION: &str = "cursor-report-position";
pub const FRIEND_REQUEST_RECEIVED: &str = "friend-request-received"; pub const FRIEND_REQUEST_RECEIVED: &str = "friend-request-received";
@@ -69,11 +85,39 @@ fn on_unfriended(payload: Payload, _socket: RawClient) {
fn on_friend_cursor_position(payload: Payload, _socket: RawClient) { fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
match payload { match payload {
Payload::Text(str) => { Payload::Text(values) => {
// values is Vec<serde_json::Value>
if let Some(first_value) = values.first() {
let incoming_data: Result<IncomingFriendCursorPayload, _> = serde_json::from_value(first_value.clone());
match incoming_data {
Ok(friend_data) => {
// We received grid coordinates (mapped)
let mapped_pos = &friend_data.position;
// Convert grid coordinates back to absolute screen coordinates (raw)
let raw_pos = grid_to_absolute_position(mapped_pos);
let outgoing_payload = OutgoingFriendCursorPayload {
user_id: friend_data.user_id.clone(),
position: CursorPositions {
raw: raw_pos,
mapped: mapped_pos.clone(),
},
};
get_app_handle() get_app_handle()
.emit(WS_EVENT::FRIEND_CURSOR_POSITION, str) .emit(WS_EVENT::FRIEND_CURSOR_POSITION, outgoing_payload)
.unwrap(); .unwrap();
} }
Err(e) => {
error!("Failed to parse friend cursor position data: {}", e);
}
}
} else {
error!("Received empty text payload for friend cursor position");
}
}
_ => error!("Received unexpected payload format for friend cursor position"), _ => error!("Received unexpected payload format for friend cursor position"),
} }
} }

View File

@@ -11,20 +11,20 @@ export let cursorPositionOnScreen = writable<CursorPositions>({
export type FriendCursorPosition = { export type FriendCursorPosition = {
userId: string; userId: string;
position: CursorPosition; position: CursorPositions;
}; };
// Map of userId -> { position: CursorPosition, lastUpdated: number } // Map of userId -> { position: CursorPositions, lastUpdated: number }
// We store the timestamp to detect stale cursors // We store the timestamp to detect stale cursors
type FriendCursorData = { type FriendCursorData = {
position: CursorPosition; position: CursorPositions;
lastUpdated: number; lastUpdated: number;
}; };
// The exported store will only expose the position part to consumers, // The exported store will only expose the position part to consumers,
// but internally we manage the full data. // but internally we manage the full data.
// Actually, it's easier if we just export the positions and manage state internally. // Actually, it's easier if we just export the positions and manage state internally.
export let friendsCursorPositions = writable<Record<string, CursorPosition>>( export let friendsCursorPositions = writable<Record<string, CursorPositions>>(
{}, {},
); );
@@ -59,27 +59,11 @@ export async function initCursorTracking() {
); );
// Listen to friend cursor position events // Listen to friend cursor position events
unlistenFriendCursor = await listen<[FriendCursorPosition]>( unlistenFriendCursor = await listen<FriendCursorPosition>(
"friend-cursor-position", "friend-cursor-position",
(event) => { (event) => {
// payload might be a string JSON if it comes directly from rust_socketio Payload::Text // We now receive a clean object from Rust
let payload = event.payload; const data = event.payload;
if (typeof payload === "string") {
try {
payload = JSON.parse(payload);
} catch (e) {
console.error(
"[Cursor] Failed to parse friend cursor position payload:",
e,
);
return;
}
}
// Rust socket.io client returns payload as an array of arguments
// Since we only send one argument { userId, position }, it's the first element
const data = Array.isArray(payload) ? payload[0] : payload;
// Update internal state with timestamp // Update internal state with timestamp
friendCursorState[data.userId] = { friendCursorState[data.userId] = {

View File

@@ -34,9 +34,18 @@
<p class="text-sm font-semibold opacity-75">Friends Online</p> <p class="text-sm font-semibold opacity-75">Friends Online</p>
<div> <div>
{#each Object.entries($friendsCursorPositions) as [userId, position]} {#each Object.entries($friendsCursorPositions) as [userId, position]}
<div class="bg-base-200/50 p-2 rounded text-xs text-left"> <div
class="bg-base-200/50 p-2 rounded text-xs text-left flex gap-2 flex-col"
>
<span class="font-bold">{getFriendName(userId)}</span> <span class="font-bold">{getFriendName(userId)}</span>
<span class="font-mono ml-2">({position.x}, {position.y})</span> <div class="flex flex-col font-mono">
<span>
Raw: ({position.raw.x}, {position.raw.y})
</span>
<span>
Mapped: ({position.mapped.x}, {position.mapped.y})
</span>
</div>
</div> </div>
{/each} {/each}
</div> </div>