fix: scaling, and chat
* Chat ergonomics vastly improved. * Scaling now done through client side settings
This commit is contained in:
parent
98f38c9714
commit
b430c80000
8 changed files with 1564 additions and 439 deletions
179
crates/chattyness-user-ui/src/components/settings.rs
Normal file
179
crates/chattyness-user-ui/src/components/settings.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
//! Scene viewer display settings with localStorage persistence.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// LocalStorage key for viewer settings.
|
||||
const SETTINGS_KEY: &str = "chattyness_viewer_settings";
|
||||
|
||||
/// Reference resolution for enlarged props calculation.
|
||||
pub const REFERENCE_WIDTH: f64 = 1920.0;
|
||||
pub const REFERENCE_HEIGHT: f64 = 1080.0;
|
||||
|
||||
/// Base size for props and avatars in scene space.
|
||||
pub const BASE_PROP_SIZE: f64 = 60.0;
|
||||
|
||||
/// Minimum zoom level (25%).
|
||||
pub const ZOOM_MIN: f64 = 0.25;
|
||||
|
||||
/// Maximum zoom level (400%).
|
||||
pub const ZOOM_MAX: f64 = 4.0;
|
||||
|
||||
/// Zoom step increment.
|
||||
pub const ZOOM_STEP: f64 = 0.25;
|
||||
|
||||
/// Pan step in pixels for keyboard navigation.
|
||||
pub const PAN_STEP: f64 = 50.0;
|
||||
|
||||
/// Calculate the minimum zoom level for pan mode.
|
||||
///
|
||||
/// - Large scenes: min zoom fills the viewport
|
||||
/// - Small scenes: min zoom is 1.0 (native resolution, centered)
|
||||
pub fn calculate_min_zoom(
|
||||
scene_width: f64,
|
||||
scene_height: f64,
|
||||
viewport_width: f64,
|
||||
viewport_height: f64,
|
||||
) -> f64 {
|
||||
if scene_width <= 0.0
|
||||
|| scene_height <= 0.0
|
||||
|| viewport_width <= 0.0
|
||||
|| viewport_height <= 0.0
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
let min_x = viewport_width / scene_width;
|
||||
let min_y = viewport_height / scene_height;
|
||||
// For large scenes: min zoom fills viewport
|
||||
// For small scenes: clamp to 1.0 (native resolution)
|
||||
min_x.max(min_y).min(1.0)
|
||||
}
|
||||
|
||||
/// Scene viewer display settings.
|
||||
///
|
||||
/// These settings control how the scene is rendered and are persisted
|
||||
/// to localStorage for user preference retention.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ViewerSettings {
|
||||
/// When true, canvas shows at native resolution with scrolling.
|
||||
/// When false, canvas scales to fit viewport (default).
|
||||
pub panning_enabled: bool,
|
||||
|
||||
/// Zoom level (0.25 to 4.0). Only applicable when `panning_enabled` is true.
|
||||
pub zoom_level: f64,
|
||||
|
||||
/// When true, props use reference scaling based on 1920x1080.
|
||||
/// Only applicable when `panning_enabled` is false.
|
||||
pub enlarge_props: bool,
|
||||
|
||||
/// Saved horizontal scroll position for pan mode.
|
||||
pub scroll_x: f64,
|
||||
|
||||
/// Saved vertical scroll position for pan mode.
|
||||
pub scroll_y: f64,
|
||||
}
|
||||
|
||||
impl Default for ViewerSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
panning_enabled: false,
|
||||
zoom_level: 1.0,
|
||||
enlarge_props: false,
|
||||
scroll_x: 0.0,
|
||||
scroll_y: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewerSettings {
|
||||
/// Load settings from localStorage, returning defaults if not found or invalid.
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn load() -> Self {
|
||||
let Some(window) = web_sys::window() else {
|
||||
return Self::default();
|
||||
};
|
||||
|
||||
let Ok(Some(storage)) = window.local_storage() else {
|
||||
return Self::default();
|
||||
};
|
||||
|
||||
let Ok(Some(json)) = storage.get_item(SETTINGS_KEY) else {
|
||||
return Self::default();
|
||||
};
|
||||
|
||||
serde_json::from_str(&json).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Stub for SSR - returns default settings.
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
pub fn load() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Save settings to localStorage.
|
||||
#[cfg(feature = "hydrate")]
|
||||
pub fn save(&self) {
|
||||
let Some(window) = web_sys::window() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(Some(storage)) = window.local_storage() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Ok(json) = serde_json::to_string(self) {
|
||||
let _ = storage.set_item(SETTINGS_KEY, &json);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stub for SSR - no-op.
|
||||
#[cfg(not(feature = "hydrate"))]
|
||||
pub fn save(&self) {}
|
||||
|
||||
/// Calculate the effective prop size based on current settings.
|
||||
///
|
||||
/// In pan mode, returns base size * zoom level.
|
||||
/// In fit mode with enlarge_props, returns size adjusted for reference resolution.
|
||||
/// Otherwise returns base size (caller should multiply by canvas scale).
|
||||
pub fn calculate_prop_size(&self, scene_width: f64, scene_height: f64) -> f64 {
|
||||
if self.panning_enabled {
|
||||
// Pan mode: base size * zoom
|
||||
BASE_PROP_SIZE * self.zoom_level
|
||||
} else if self.enlarge_props {
|
||||
// Reference scaling: ensure minimum size based on 1920x1080
|
||||
let scale_w = scene_width / REFERENCE_WIDTH;
|
||||
let scale_h = scene_height / REFERENCE_HEIGHT;
|
||||
BASE_PROP_SIZE * scale_w.max(scale_h)
|
||||
} else {
|
||||
// Default: base size (will be scaled by canvas scale factor)
|
||||
BASE_PROP_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
/// Adjust zoom level by a delta, clamping to valid range.
|
||||
///
|
||||
/// If `min_zoom` is provided, uses that as the floor instead of `ZOOM_MIN`.
|
||||
pub fn adjust_zoom(&mut self, delta: f64) {
|
||||
self.zoom_level = (self.zoom_level + delta).clamp(ZOOM_MIN, ZOOM_MAX);
|
||||
}
|
||||
|
||||
/// Adjust zoom level with a custom minimum.
|
||||
pub fn adjust_zoom_with_min(&mut self, delta: f64, min_zoom: f64) {
|
||||
let effective_min = min_zoom.max(ZOOM_MIN);
|
||||
self.zoom_level = (self.zoom_level + delta).clamp(effective_min, ZOOM_MAX);
|
||||
}
|
||||
|
||||
/// Clamp zoom level to an effective minimum.
|
||||
pub fn clamp_zoom_min(&mut self, min_zoom: f64) {
|
||||
let effective_min = min_zoom.max(ZOOM_MIN);
|
||||
if self.zoom_level < effective_min {
|
||||
self.zoom_level = effective_min;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset scroll position to origin.
|
||||
pub fn reset_scroll(&mut self) {
|
||||
self.scroll_x = 0.0;
|
||||
self.scroll_y = 0.0;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue