//! Overlay components for prop editing in scene viewer. //! //! Contains ScaleOverlay and MoveOverlay components used when //! moderators edit prop scale or position. use leptos::prelude::*; use uuid::Uuid; use chattyness_db::models::LooseProp; use super::super::settings::{BASE_AVATAR_SCALE, BASE_PROP_SCALE}; #[cfg(feature = "hydrate")] use super::super::canvas_utils::normalize_asset_path; /// Overlay shown when editing a prop's scale. /// /// Allows dragging from the prop center to adjust scale. #[component] pub fn ScaleOverlay( #[prop(into)] active: Signal, #[prop(into)] prop_id: Signal>, #[prop(into)] preview_scale: RwSignal, #[prop(into)] prop_center: Signal<(f64, f64)>, #[prop(into)] loose_props: Signal>, #[prop(into)] prop_size: Signal, #[prop(optional)] on_apply: Option>, #[prop(optional)] on_cancel: Option>, ) -> impl IntoView { let (_center_x, _center_y) = prop_center.get_untracked(); view! { {move || { let current_prop_id = prop_id.get(); let current_preview_scale = preview_scale.get(); let (center_x, center_y) = prop_center.get(); // Find the prop to get its dimensions let prop_data = current_prop_id.and_then(|id| { loose_props.get().iter().find(|p| p.id == id).cloned() }); view! {
// Visual feedback: dashed border around prop {move || { if let Some(ref _prop) = prop_data { let base_size = prop_size.get(); let prop_scale_ratio = BASE_PROP_SCALE / BASE_AVATAR_SCALE; let preview_prop_size = base_size * prop_scale_ratio * current_preview_scale as f64; let half_size = preview_prop_size / 2.0; view! {
// Scale indicator
{format!("{:.2}x", current_preview_scale)}
}.into_any() } else { ().into_any() } }} // Instructions
"Drag to resize • Release to apply • Escape to cancel"
} }} } } /// Overlay shown when moving a prop to a new position. #[component] pub fn MoveOverlay( #[prop(into)] active: Signal, #[prop(into)] prop_id: Signal>, #[prop(into)] preview_position: RwSignal<(f64, f64)>, #[prop(into)] prop_scale: Signal, #[prop(into)] loose_props: Signal>, #[prop(into)] prop_size: Signal, #[prop(into)] scale_x: Signal, #[prop(into)] scale_y: Signal, #[prop(into)] offset_x: Signal, #[prop(into)] offset_y: Signal, #[prop(optional)] on_apply: Option>, #[prop(optional)] on_cancel: Option>, ) -> impl IntoView { view! { {move || { let current_prop_id = prop_id.get(); let (_preview_x, _preview_y) = preview_position.get(); let current_prop_scale = prop_scale.get(); // Find the prop to get its asset path let prop_data = current_prop_id.and_then(|id| { loose_props.get().iter().find(|p| p.id == id).cloned() }); // Calculate ghost size let base_size = prop_size.get(); let prop_scale_ratio = BASE_PROP_SCALE / BASE_AVATAR_SCALE; let ghost_size = base_size * prop_scale_ratio * current_prop_scale as f64; view! {
0.0 && sy > 0.0 { let scene_x = (viewer_x - ox) / sx; let scene_y = (viewer_y - oy) / sy; preview_position.set((scene_x, scene_y)); } } } #[cfg(not(feature = "hydrate"))] let _ = ev; } on:click=move |ev| { #[cfg(feature = "hydrate")] { if let (Some(pid), Some(ref callback)) = (prop_id.get(), on_apply.as_ref()) { let (final_x, final_y) = preview_position.get(); callback.run((pid, final_x, final_y)); } if let Some(ref callback) = on_cancel { callback.run(()); } } #[cfg(not(feature = "hydrate"))] let _ = ev; } on:keydown=move |ev| { #[cfg(feature = "hydrate")] { use wasm_bindgen::JsCast; let ev: web_sys::KeyboardEvent = ev.dyn_into().unwrap(); if ev.key() == "Escape" { ev.prevent_default(); if let Some(ref callback) = on_cancel { callback.run(()); } } } #[cfg(not(feature = "hydrate"))] let _ = ev; } tabindex="0" > // Ghost prop at cursor position {move || { #[cfg(feature = "hydrate")] { if let Some(ref prop) = prop_data { let (preview_x, preview_y) = preview_position.get(); let sx = scale_x.get(); let sy = scale_y.get(); let ox = offset_x.get(); let oy = offset_y.get(); // Get scene viewer position in viewport let document = web_sys::window().unwrap().document().unwrap(); let viewer_offset = document .query_selector(".scene-viewer-container") .ok() .flatten() .map(|v| { let rect = v.get_bounding_client_rect(); (rect.left(), rect.top()) }) .unwrap_or((0.0, 0.0)); // Convert scene coords to viewport coords let viewer_x = preview_x * sx + ox; let viewer_y = preview_y * sy + oy; let viewport_x = viewer_x + viewer_offset.0; let viewport_y = viewer_y + viewer_offset.1; let normalized_path = normalize_asset_path(&prop.prop_asset_path); view! {
}.into_any() } else { ().into_any() } } #[cfg(not(feature = "hydrate"))] { ().into_any() } }} // Instructions
"Click to place • Escape to cancel"
} }}
} }