//! Scene viewer display settings with localStorage persistence. use serde::{Deserialize, Serialize}; use crate::utils::LocalStoragePersist; /// Reference resolution for enlarged props calculation. pub const REFERENCE_WIDTH: f64 = 1920.0; pub const REFERENCE_HEIGHT: f64 = 1080.0; /// Base size for props/avatars in scene coordinates. /// SVG assets are 120x120 pixels - this is the native/full size. pub const BASE_PROP_SIZE: f64 = 120.0; /// Scale factor for avatar rendering relative to BASE_PROP_SIZE. /// Avatars render at 50% (60px cells) to allow merit-based scaling up later. pub const BASE_AVATAR_SCALE: f64 = 0.5; /// Scale factor for dropped loose props relative to BASE_PROP_SIZE. /// Props render at 75% (90px) at default scale=1.0. pub const BASE_PROP_SCALE: f64 = 0.75; /// 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; /// Minimum text em size (50%). pub const TEXT_EM_MIN: f64 = 0.5; /// Maximum text em size (200%). pub const TEXT_EM_MAX: f64 = 2.0; /// Text em size step increment. pub const TEXT_EM_STEP: f64 = 0.1; /// 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. /// Applies in both fit mode and pan mode. 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, /// Text size multiplier for display names, chat bubbles, and badges. /// Range: 0.5 to 2.0 (50% to 200%). pub text_em_size: f64, } impl Default for ViewerSettings { fn default() -> Self { Self { panning_enabled: false, zoom_level: 1.0, enlarge_props: true, scroll_x: 0.0, scroll_y: 0.0, text_em_size: 1.0, } } } // Implement LocalStoragePersist trait for automatic load/save impl LocalStoragePersist for ViewerSettings { const STORAGE_KEY: &'static str = "chattyness_viewer_settings"; } impl ViewerSettings { /// Calculate the effective prop size based on current settings. /// /// In pan mode without enlarge, returns base size * zoom level. /// In pan mode with enlarge, returns base size * reference_scale * 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 { // Reference scale factor for "enlarge props" mode let ref_scale = (scene_width / REFERENCE_WIDTH).max(scene_height / REFERENCE_HEIGHT); if self.panning_enabled { if self.enlarge_props { BASE_PROP_SIZE * ref_scale * self.zoom_level } else { BASE_PROP_SIZE * self.zoom_level } } else if self.enlarge_props { // Reference scaling: ensure minimum size based on 1920x1080 BASE_PROP_SIZE * ref_scale } 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; } /// Adjust text em size by a delta, clamping to valid range. pub fn adjust_text_em(&mut self, delta: f64) { self.text_em_size = (self.text_em_size + delta).clamp(TEXT_EM_MIN, TEXT_EM_MAX); } }