chattyness/crates/chattyness-user-ui/src/components/settings.rs
Evan Carroll a2841c413d Fix prop renders
* Incorporate prop scaling
* Props now render to a canvas
2026-01-23 16:02:23 -06:00

169 lines
5.5 KiB
Rust

//! 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);
}
}