petpet headpat init
This commit is contained in:
@@ -6,6 +6,7 @@ pub mod dolls;
|
||||
pub mod friends;
|
||||
pub mod interaction;
|
||||
pub mod sprite;
|
||||
pub mod petpet;
|
||||
|
||||
use crate::lock_r;
|
||||
use crate::state::{init_app_data_scoped, AppDataRefreshScope, FDOLL};
|
||||
|
||||
7
src-tauri/src/commands/petpet.rs
Normal file
7
src-tauri/src/commands/petpet.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
use crate::models::dolls::DollDto;
|
||||
use crate::services::petpet;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn encode_pet_doll_gif_base64(doll: DollDto) -> Result<String, String> {
|
||||
petpet::encode_pet_doll_gif_base64(doll)
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use commands::friends::{
|
||||
};
|
||||
use commands::interaction::send_interaction_cmd;
|
||||
use commands::sprite::recolor_gif_base64;
|
||||
use commands::petpet::encode_pet_doll_gif_base64;
|
||||
use tauri::async_runtime;
|
||||
|
||||
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
||||
@@ -74,6 +75,7 @@ pub fn run() {
|
||||
set_active_doll,
|
||||
remove_active_doll,
|
||||
recolor_gif_base64,
|
||||
encode_pet_doll_gif_base64,
|
||||
quit_app,
|
||||
restart_app,
|
||||
retry_connection,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use serde_json::json;
|
||||
use tracing::{error, info};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{
|
||||
lock_r, models::interaction::SendInteractionDto, services::ws::WS_EVENT, state::FDOLL,
|
||||
@@ -25,23 +25,12 @@ pub async fn send_interaction(dto: SendInteractionDto) -> Result<(), String> {
|
||||
// The DTO structure matches what the server expects:
|
||||
// { recipientUserId, content, type } (handled by serde rename_all="camelCase")
|
||||
|
||||
// Note: The `type` field in DTO is mapped to `type_` in Rust struct but serialized as `type`
|
||||
// due to camelCase renaming (if we rely on TS-RS output) or manual renaming.
|
||||
// Wait, `type` is a reserved keyword in Rust so we used `type_`.
|
||||
// We need to ensure serialization maps `type_` to `type`.
|
||||
// However, serde's `rename_all = "camelCase"` handles `recipient_user_id` -> `recipientUserId`.
|
||||
// It does NOT automatically handle `type_` -> `type`.
|
||||
// We should add `#[serde(rename = "type")]` to the `type_` field in the model.
|
||||
// I will fix the model first to ensure correct serialization.
|
||||
|
||||
let payload = json!({
|
||||
"recipientUserId": dto.recipient_user_id,
|
||||
"content": dto.content,
|
||||
"type": dto.type_
|
||||
});
|
||||
|
||||
info!("Sending interaction via WS: {:?}", payload);
|
||||
|
||||
// Blocking emission because rust_socketio::Client::emit is synchronous/blocking
|
||||
// but we are in an async context. Ideally we spawn_blocking.
|
||||
let spawn_result = tauri::async_runtime::spawn_blocking(move || {
|
||||
|
||||
@@ -13,6 +13,7 @@ pub mod interaction;
|
||||
pub mod presence_modules;
|
||||
pub mod scene;
|
||||
pub mod sprite_recolor;
|
||||
pub mod petpet;
|
||||
pub mod welcome;
|
||||
pub mod ws;
|
||||
|
||||
|
||||
25
src-tauri/src/services/petpet/mod.rs
Normal file
25
src-tauri/src/services/petpet/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::models::dolls::DollDto;
|
||||
use crate::services::sprite_recolor;
|
||||
use image::{imageops::FilterType, RgbaImage};
|
||||
use petpet::{encode_gif, generate};
|
||||
|
||||
pub fn encode_pet_doll_gif_base64(doll: DollDto) -> Result<String, String> {
|
||||
let body_color = &doll.configuration.color_scheme.body;
|
||||
let outline_color = &doll.configuration.color_scheme.outline;
|
||||
|
||||
// Get recolored image
|
||||
let img: RgbaImage = sprite_recolor::get_recolored_image(body_color, outline_color)
|
||||
.map_err(|e| format!("Failed to recolor image: {}", e))?;
|
||||
|
||||
// Generate petpet frames
|
||||
let frames = generate(img, FilterType::Lanczos3, None)
|
||||
.map_err(|e| format!("Failed to generate petpet frames: {}", e))?;
|
||||
|
||||
// Encode to GIF
|
||||
let mut output = Vec::new();
|
||||
encode_gif(frames, &mut output, 10).map_err(|e| format!("Failed to encode GIF: {}", e))?;
|
||||
|
||||
// Base64
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
Ok(general_purpose::STANDARD.encode(&output))
|
||||
}
|
||||
@@ -226,3 +226,30 @@ fn pick_color_from_palette(palette: &[Rgba<u8>], x: u32, y: u32) -> Rgba<u8> {
|
||||
// SAFETY: index is guaranteed to be < palette.len() due to modulo operation
|
||||
unsafe { *palette.get_unchecked(index) }
|
||||
}
|
||||
|
||||
pub fn get_recolored_image(
|
||||
white_color_hex: &str,
|
||||
black_color_hex: &str,
|
||||
) -> Result<RgbaImage, Box<dyn std::error::Error>> {
|
||||
let white_color = parse_hex_color(white_color_hex)?;
|
||||
let black_color = parse_hex_color(black_color_hex)?;
|
||||
|
||||
// Get pre-decoded GIF data
|
||||
let decoded_gif = get_decoded_gif();
|
||||
|
||||
// Take first frame
|
||||
let frame = &decoded_gif.frames[0];
|
||||
let mut img = RgbaImage::from_raw(
|
||||
decoded_gif.width as u32,
|
||||
decoded_gif.height as u32,
|
||||
frame.pixels.clone(),
|
||||
)
|
||||
.ok_or("Failed to create image from frame")?;
|
||||
|
||||
// Recolor the image
|
||||
let white_palette = generate_color_palette(white_color, 7);
|
||||
let black_palette = generate_color_palette(black_color, 7);
|
||||
recolor_image(&mut img, &white_palette, &black_palette, true);
|
||||
|
||||
Ok(img)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user