//! Persisted user preferences — DYN, POLAR (+ increment), OTRACK, and assorted //! app-level flags (backup, autosave, plugin lists, viewport background). These //! are UI choices, not drawing data, so they live in the consolidated per-user //! config ([`crate::app::config`], the "settings" section) and survive across //! sessions. Drawing-scoped state (Ortho `$ORTHOMODE`, running OSNAP `$OSMODE`, //! lineweight display `$LWDISPLAY`, …) belongs to the file, not here. //! //! Also home to the `$OSMODE` bit conversions ([`osmode_from_snaps`] / //! [`snaps_from_osmode`]) that bridge the running-snap set and the drawing header. use crate::snap::SnapType; use serde::{Deserialize, Serialize}; /// Canonical order of the user-toggleable object-snap modes. Drives the /// deterministic order when decoding the `$OSMODE` bitmask (see /// [`snaps_from_osmode`]). const SNAP_ORDER: &[SnapType] = &[ SnapType::Endpoint, SnapType::Midpoint, SnapType::Center, SnapType::Node, SnapType::Quadrant, SnapType::Intersection, SnapType::Extension, SnapType::Insertion, SnapType::Perpendicular, SnapType::Tangent, SnapType::Nearest, SnapType::ApparentIntersection, SnapType::Parallel, // Grid snap (SNAPMODE) is a per-drawing view setting stored on the VPort, // not a global OSNAP preference, so it is deliberately excluded from the // persisted set. (#121) ]; /// `$OSMODE` bit for each running object-snap mode (standard AutoCAD bitmask). /// `None` for OCS-only snaps (Grid, ObjectPick) that have no standard bit. fn snap_bit(s: SnapType) -> Option { Some(match s { SnapType::Endpoint => 1, SnapType::Midpoint => 2, SnapType::Center => 4, SnapType::Node => 8, SnapType::Quadrant => 16, SnapType::Intersection => 32, SnapType::Insertion => 64, SnapType::Perpendicular => 128, SnapType::Tangent => 256, SnapType::Nearest => 512, SnapType::ApparentIntersection => 2048, SnapType::Extension => 4096, SnapType::Parallel => 8192, SnapType::Grid | SnapType::ObjectPick => return None, }) } /// Bit 14 of `$OSMODE` — object snap turned off (master suppress). const OSMODE_SUPPRESS: i32 = 16384; /// Encode the running-snap set + master toggle into an `$OSMODE` bitmask for the /// drawing header. OCS-only snaps (Grid, ObjectPick) have no bit and are /// dropped. A cleared master toggle sets the suppress bit. pub(crate) fn osmode_from_snaps<'a>( enabled: impl IntoIterator, snap_enabled: bool, ) -> i32 { let mut bits = 0; for t in enabled { if let Some(b) = snap_bit(*t) { bits |= b; } } if !snap_enabled { bits |= OSMODE_SUPPRESS; } bits } /// Decode an `$OSMODE` bitmask into `(running-snap set, master enabled)`. Only /// the standard mappable modes are produced; the suppress bit maps (inverted) to /// the master toggle. pub(crate) fn snaps_from_osmode(osmode: i32) -> (Vec, bool) { let modes = SNAP_ORDER .iter() .copied() .filter(|t| snap_bit(*t).is_some_and(|b| osmode & b != 0)) .collect(); (modes, osmode & OSMODE_SUPPRESS == 0) } /// The "settings" section of the consolidated config ([`crate::app::config`]). /// Field defaults mirror the app's in-code defaults so a missing key restores /// the value the app boots with. #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(default)] pub struct UserSettings { pub dyn_input: bool, pub polar: bool, pub polar_increment_deg: f32, pub otrack: bool, // Ortho ($ORTHOMODE) and the running OSNAP set ($OSMODE) are per-drawing — // stored in the document header, not here (they used to be persisted app- // globally, which duplicated the file's own state). /// Whether the one-time "make Open CAD Studio the default for .dwg/.dxf?" /// prompt has already been shown. Set once the user answers (either way), /// so we never nag again on subsequent launches. pub default_assoc_prompted: bool, /// Ids of plugins the user turned off in the Plugin Manager. Disabled /// plugins keep their manifest listed but drop their ribbon tab and command /// dispatch. pub disabled_plugins: Vec, /// Linked plugin source repositories (`owner/repo`) the marketplace installs /// from. pub plugin_repos: Vec, /// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single). pub texteditmode: bool, /// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false). pub textfill: bool, /// When true, saving over an existing file first copies it to a sibling /// `.bak` so a faulty or accidental save can be recovered (#205). pub backup_on_save: bool, /// When true (default), the app (re)registers itself as a .dwg/.dxf/.bak /// handler on every launch. Toggle with the FILEASSOC command. pub file_assoc_enabled: bool, /// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command). /// 0 disables autosave. pub savetime_min: i32, /// Persisted viewport background colours (0–255 RGB); `None` = app default /// (dark grey model / off-white paper). Applied to every drawing tab on /// launch and to tabs opened later, so a chosen background survives restarts /// (#188). pub bg_color: Option<[u8; 3]>, pub paper_bg_color: Option<[u8; 3]>, } impl Default for UserSettings { fn default() -> Self { Self { dyn_input: true, polar: false, polar_increment_deg: 45.0, otrack: false, default_assoc_prompted: false, disabled_plugins: Vec::new(), plugin_repos: Vec::new(), texteditmode: false, textfill: true, backup_on_save: true, file_assoc_enabled: true, savetime_min: 10, bg_color: None, paper_bg_color: None, } } } #[cfg(test)] mod tests { use super::*; #[test] fn osmode_encodes_bits_and_suppress() { // Endpoint(1) + Midpoint(2) + Intersection(32) = 35, master on. let on = [SnapType::Endpoint, SnapType::Midpoint, SnapType::Intersection]; assert_eq!(osmode_from_snaps(on.iter(), true), 35); // Master off sets the suppress bit (16384). assert_eq!(osmode_from_snaps(on.iter(), false), 35 | 16384); // OCS-only snaps carry no bit and are dropped. assert_eq!(osmode_from_snaps([SnapType::Grid, SnapType::ObjectPick].iter(), true), 0); } #[test] fn osmode_decodes_bits_and_suppress() { let (modes, enabled) = snaps_from_osmode(35); let set: std::collections::HashSet<_> = modes.into_iter().collect(); assert!(enabled); assert_eq!(set.len(), 3); assert!(set.contains(&SnapType::Endpoint)); assert!(set.contains(&SnapType::Midpoint)); assert!(set.contains(&SnapType::Intersection)); // Suppress bit → master off; the mode bits still decode. let (_m, en) = snaps_from_osmode(35 | 16384); assert!(!en); } #[test] fn osmode_round_trips_every_mappable_mode() { // All 13 running snaps + master on encode and decode back to the same set. let all: Vec = SNAP_ORDER.to_vec(); let bits = osmode_from_snaps(all.iter(), true); let (back, enabled) = snaps_from_osmode(bits); assert!(enabled); let a: std::collections::HashSet<_> = all.into_iter().collect(); let b: std::collections::HashSet<_> = back.into_iter().collect(); assert_eq!(a, b); } }