clean up scene_viewer
This commit is contained in:
parent
2afe43547d
commit
fe40fd32ab
4 changed files with 958 additions and 958 deletions
|
|
@ -0,0 +1,306 @@
|
|||
//! 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<bool>,
|
||||
#[prop(into)] prop_id: Signal<Option<Uuid>>,
|
||||
#[prop(into)] preview_scale: RwSignal<f32>,
|
||||
#[prop(into)] prop_center: Signal<(f64, f64)>,
|
||||
#[prop(into)] loose_props: Signal<Vec<LooseProp>>,
|
||||
#[prop(into)] prop_size: Signal<f64>,
|
||||
#[prop(optional)] on_apply: Option<Callback<(Uuid, f32)>>,
|
||||
#[prop(optional)] on_cancel: Option<Callback<()>>,
|
||||
) -> impl IntoView {
|
||||
let (_center_x, _center_y) = prop_center.get_untracked();
|
||||
|
||||
view! {
|
||||
<Show when=move || active.get()>
|
||||
{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! {
|
||||
<div
|
||||
class="fixed inset-0 z-50 cursor-crosshair"
|
||||
style="background: rgba(0,0,0,0.3);"
|
||||
on:mousemove=move |ev| {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
use wasm_bindgen::JsCast;
|
||||
let ev: web_sys::MouseEvent = ev.dyn_into().unwrap();
|
||||
let mouse_x = ev.client_x() as f64;
|
||||
let mouse_y = ev.client_y() as f64;
|
||||
let (cx, cy) = prop_center.get();
|
||||
let dx = mouse_x - cx;
|
||||
let dy = mouse_y - cy;
|
||||
let distance = (dx * dx + dy * dy).sqrt();
|
||||
// Scale formula: distance / 40 gives 1x at 40px
|
||||
let new_scale = (distance / 40.0).clamp(0.1, 10.0) as f32;
|
||||
preview_scale.set(new_scale);
|
||||
}
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
let _ = ev;
|
||||
}
|
||||
on:mouseup=move |ev| {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
if let (Some(pid), Some(ref callback)) = (prop_id.get(), on_apply.as_ref()) {
|
||||
let final_scale = preview_scale.get();
|
||||
callback.run((pid, final_scale));
|
||||
}
|
||||
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"
|
||||
>
|
||||
// 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! {
|
||||
<div
|
||||
class="absolute pointer-events-none"
|
||||
style=format!(
|
||||
"left: {}px; top: {}px; width: {}px; height: {}px; \
|
||||
border: 2px dashed #fbbf24; \
|
||||
transform: translate(-50%, -50%); \
|
||||
box-sizing: border-box;",
|
||||
center_x, center_y, preview_prop_size, preview_prop_size
|
||||
)
|
||||
/>
|
||||
// Scale indicator
|
||||
<div
|
||||
class="absolute bg-gray-900/90 text-yellow-400 px-2 py-1 rounded text-sm font-mono pointer-events-none"
|
||||
style=format!(
|
||||
"left: {}px; top: {}px; transform: translate(-50%, 8px);",
|
||||
center_x, center_y + half_size
|
||||
)
|
||||
>
|
||||
{format!("{:.2}x", current_preview_scale)}
|
||||
</div>
|
||||
}.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}}
|
||||
// Instructions
|
||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-gray-900/90 text-white px-4 py-2 rounded text-sm">
|
||||
"Drag to resize • Release to apply • Escape to cancel"
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
|
||||
/// Overlay shown when moving a prop to a new position.
|
||||
#[component]
|
||||
pub fn MoveOverlay(
|
||||
#[prop(into)] active: Signal<bool>,
|
||||
#[prop(into)] prop_id: Signal<Option<Uuid>>,
|
||||
#[prop(into)] preview_position: RwSignal<(f64, f64)>,
|
||||
#[prop(into)] prop_scale: Signal<f32>,
|
||||
#[prop(into)] loose_props: Signal<Vec<LooseProp>>,
|
||||
#[prop(into)] prop_size: Signal<f64>,
|
||||
#[prop(into)] scale_x: Signal<f64>,
|
||||
#[prop(into)] scale_y: Signal<f64>,
|
||||
#[prop(into)] offset_x: Signal<f64>,
|
||||
#[prop(into)] offset_y: Signal<f64>,
|
||||
#[prop(optional)] on_apply: Option<Callback<(Uuid, f64, f64)>>,
|
||||
#[prop(optional)] on_cancel: Option<Callback<()>>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<Show when=move || active.get()>
|
||||
{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! {
|
||||
<div
|
||||
class="fixed inset-0 z-50 cursor-crosshair"
|
||||
style="background: rgba(0,0,0,0.3);"
|
||||
on:mousemove=move |ev| {
|
||||
#[cfg(feature = "hydrate")]
|
||||
{
|
||||
use wasm_bindgen::JsCast;
|
||||
let ev: web_sys::MouseEvent = ev.dyn_into().unwrap();
|
||||
let mouse_x = ev.client_x() as f64;
|
||||
let mouse_y = ev.client_y() as f64;
|
||||
|
||||
// Get scene viewer's position
|
||||
let document = web_sys::window().unwrap().document().unwrap();
|
||||
if let Some(viewer) = document.query_selector(".scene-viewer-container").ok().flatten() {
|
||||
let rect = viewer.get_bounding_client_rect();
|
||||
let viewer_x = mouse_x - rect.left();
|
||||
let viewer_y = mouse_y - rect.top();
|
||||
|
||||
let sx = scale_x.get();
|
||||
let sy = scale_y.get();
|
||||
let ox = offset_x.get();
|
||||
let oy = offset_y.get();
|
||||
|
||||
if sx > 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! {
|
||||
<div
|
||||
class="absolute pointer-events-none"
|
||||
style=format!(
|
||||
"left: {}px; top: {}px; width: {}px; height: {}px; \
|
||||
transform: translate(-50%, -50%); \
|
||||
border: 2px dashed #10b981; \
|
||||
background: rgba(16, 185, 129, 0.2); \
|
||||
box-sizing: border-box;",
|
||||
viewport_x, viewport_y, ghost_size, ghost_size
|
||||
)
|
||||
>
|
||||
<img
|
||||
src=normalized_path
|
||||
class="w-full h-full object-contain opacity-50"
|
||||
style="pointer-events: none;"
|
||||
/>
|
||||
</div>
|
||||
}.into_any()
|
||||
} else {
|
||||
().into_any()
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
{
|
||||
().into_any()
|
||||
}
|
||||
}}
|
||||
// Instructions
|
||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-gray-900/90 text-white px-4 py-2 rounded text-sm">
|
||||
"Click to place • Escape to cancel"
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}}
|
||||
</Show>
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue