From 66fc30429c26434f0e519c08cb4ca7ab2cd8c774 Mon Sep 17 00:00:00 2001 From: Kevin Griffin <117586586+KevinGriffin-new@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:21:46 -0700 Subject: [PATCH] feat(background): persist viewport background across sessions (#188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BACKGROUND command set a per-tab colour that was lost on restart and didn't apply to new drawings, so there was no way to make a non-black background stick. Persist the chosen model/paper background in UserSettings (settings.txt) and re-apply it on launch and to every drawing tab created afterward. - settings.rs: persist `bg_color` / `paper_bg_color` as 0–255 RGB triplets. - App gains `default_bg_color` / `default_paper_bg_color`; current_settings / apply_settings round-trip them, and apply_settings pushes the restored colour onto existing tabs. New/opened/replaced tabs call `apply_bg_default`. - BACKGROUND updates the persisted default, saved centrally via persist_settings_if_changed; RESET clears it. Co-Authored-By: Claude Opus 4.8 --- src/app/commands.rs | 18 +++++++++++---- src/app/mod.rs | 8 +++++++ src/app/settings.rs | 33 +++++++++++++++++++++++++- src/app/update.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 5 deletions(-) diff --git a/src/app/commands.rs b/src/app/commands.rs index aa09970f9..df91c1131 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -108,6 +108,9 @@ impl OpenCADStudio { // ── Background color ─────────────────────────────────────────── // Usage: BACKGROUND (0–255 each) // BACKGROUND RESET (restore default) + // The chosen colour is also stored as the persisted default + // (`default_bg_color` / `default_paper_bg_color`) so it survives + // restarts and applies to new drawings (#188). cmd if cmd == "BACKGROUND" || cmd.starts_with("BACKGROUND ") => { let args = cmd.split_whitespace().skip(1).collect::>(); let is_paper = self.tabs[i].scene.current_layout != "Model"; @@ -119,9 +122,11 @@ impl OpenCADStudio { if is_paper { self.tabs[i].paper_bg_color = None; self.tabs[i].scene.paper_bg_color = [1.0, 1.0, 1.0, 1.0]; + self.default_paper_bg_color = None; } else { self.tabs[i].bg_color = None; self.tabs[i].scene.bg_color = [0.11, 0.11, 0.11, 1.0]; + self.default_bg_color = None; } // Wire colour adaptation (`adapt_to_bg`) reads the bg // at tessellation time, so the cached wires need to @@ -139,12 +144,15 @@ impl OpenCADStudio { let r = args[0].parse::().unwrap_or(0) as f32 / 255.0; let g = args[1].parse::().unwrap_or(0) as f32 / 255.0; let b = args[2].parse::().unwrap_or(0) as f32 / 255.0; + let rgba = [r, g, b, 1.0]; if is_paper { - self.tabs[i].paper_bg_color = Some([r, g, b, 1.0]); - self.tabs[i].scene.paper_bg_color = [r, g, b, 1.0]; + self.tabs[i].paper_bg_color = Some(rgba); + self.tabs[i].scene.paper_bg_color = rgba; + self.default_paper_bg_color = Some(rgba); } else { - self.tabs[i].bg_color = Some([r, g, b, 1.0]); - self.tabs[i].scene.bg_color = [r, g, b, 1.0]; + self.tabs[i].bg_color = Some(rgba); + self.tabs[i].scene.bg_color = rgba; + self.default_bg_color = Some(rgba); } self.tabs[i].scene.recolor_meshes(); self.tabs[i].scene.bump_geometry(); @@ -152,6 +160,8 @@ impl OpenCADStudio { "Background: rgb({}, {}, {})", args[0], args[1], args[2] )); + // Persisted centrally after this message via + // `persist_settings_if_changed()`. } else { self.command_line .push_info("Usage: BACKGROUND (0–255) | BACKGROUND RESET"); diff --git a/src/app/mod.rs b/src/app/mod.rs index 2f79ea93c..7a4b9e8bf 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -245,6 +245,12 @@ pub(super) struct OpenCADStudio { dyn_input: bool, /// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single). pub texteditmode: bool, + /// Persisted default viewport background, restored from settings and applied + /// to every drawing tab (new and opened) so a chosen background survives + /// restarts (#188). `None` = the built-in dark-grey / off-white defaults. + /// The `a` channel is always 1.0. + default_bg_color: Option<[f32; 4]>, + default_paper_bg_color: Option<[f32; 4]>, /// `true` after a bare `VPORTS` in model space — the next command-line /// entry is treated as the tiled-config option (SIngle/2H/2V/4). awaiting_vports: bool, @@ -1770,6 +1776,8 @@ impl OpenCADStudio { show_grid: false, dyn_input: true, texteditmode: false, + default_bg_color: None, + default_paper_bg_color: None, awaiting_vports: false, tile_drag: None, dyn_user_reshaped: false, diff --git a/src/app/settings.rs b/src/app/settings.rs index 5dca802f1..e5f834fe2 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -58,6 +58,25 @@ fn snap_from_id(s: &str) -> Option { SNAP_ORDER.iter().copied().find(|t| snap_id(*t) == s) } +/// Parse a persisted `r,g,b` background triplet (each 0–255). Returns `None` +/// for an empty or malformed value so a missing/garbage key falls back to the +/// app default rather than a wrong colour. +fn parse_rgb(val: &str) -> Option<[u8; 3]> { + let mut it = val.split(',').map(|t| t.trim().parse::()); + let r = it.next()?.ok()?; + let g = it.next()?.ok()?; + let b = it.next()?.ok()?; + if it.next().is_some() { + return None; + } + Some([r, g, b]) +} + +/// Serialize an optional background triplet back to `r,g,b`, or empty when unset. +fn rgb_to_str(c: Option<[u8; 3]>) -> String { + c.map(|[r, g, b]| format!("{r},{g},{b}")).unwrap_or_default() +} + /// A snapshot of the persisted preferences. Field defaults mirror the app's /// in-code defaults so a missing key restores the same value the app boots /// with. @@ -84,6 +103,12 @@ pub struct UserSettings { pub plugin_repos: Vec, /// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single). pub texteditmode: bool, + /// 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 { @@ -108,6 +133,8 @@ impl Default for UserSettings { disabled_plugins: Vec::new(), plugin_repos: Vec::new(), texteditmode: false, + bg_color: None, + paper_bg_color: None, } } } @@ -140,6 +167,8 @@ impl UserSettings { } "osnap" => s.snap_enabled = val == "1", "otrack" => s.otrack = val == "1", + "bg_color" => s.bg_color = parse_rgb(val), + "paper_bg_color" => s.paper_bg_color = parse_rgb(val), "default_assoc_prompted" => s.default_assoc_prompted = val == "1", "texteditmode" => { if let Some(v) = @@ -189,7 +218,7 @@ impl UserSettings { .collect::>() .join(","); let body = format!( - "dyn={}\northo={}\npolar={}\npolar_increment_deg={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\nplugin_repos={}\ntexteditmode={}\n", + "dyn={}\northo={}\npolar={}\npolar_increment_deg={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\nplugin_repos={}\ntexteditmode={}\nbg_color={}\npaper_bg_color={}\n", b(self.dyn_input), b(self.ortho), b(self.polar), @@ -201,6 +230,8 @@ impl UserSettings { self.disabled_plugins.join(","), self.plugin_repos.join(","), self.texteditmode, + rgb_to_str(self.bg_color), + rgb_to_str(self.paper_bg_color), ); let _ = std::fs::write(path, body); } diff --git a/src/app/update.rs b/src/app/update.rs index 134b5853c..1a686a06d 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -196,6 +196,8 @@ impl OpenCADStudio { }, plugin_repos: self.plugin_repos.clone(), texteditmode: self.texteditmode, + bg_color: self.default_bg_color.map(f4_to_u3), + paper_bg_color: self.default_paper_bg_color.map(f4_to_u3), } } @@ -212,9 +214,42 @@ impl OpenCADStudio { self.disabled_plugins = s.disabled_plugins.iter().cloned().collect(); self.plugin_repos = s.plugin_repos.clone(); self.texteditmode = s.texteditmode; + self.default_bg_color = s.bg_color.map(u3_to_f4); + self.default_paper_bg_color = s.paper_bg_color.map(u3_to_f4); + // Push the restored background onto every drawing tab that exists now + // (the start tab and any initial drawing). Tabs created later pick it + // up via `apply_bg_default` at their construction site. + for idx in 0..self.tabs.len() { + self.apply_bg_default(idx); + } self.rebuild_ribbon_modules(); } + /// Apply the persisted default background(s) to tab `idx`. No-op for the + /// start tab or when no default is set. Refreshes the tab's cached wires + /// and meshes so background-adaptive colours pick up the change. + pub(super) fn apply_bg_default(&mut self, idx: usize) { + let bg = self.default_bg_color; + let paper_bg = self.default_paper_bg_color; + if bg.is_none() && paper_bg.is_none() { + return; + } + let tab = &mut self.tabs[idx]; + if tab.is_start { + return; + } + if let Some(c) = bg { + tab.bg_color = Some(c); + tab.scene.bg_color = c; + } + if let Some(c) = paper_bg { + tab.paper_bg_color = Some(c); + tab.scene.paper_bg_color = c; + } + tab.scene.recolor_meshes(); + tab.scene.bump_geometry(); + } + /// Check if a suspended command exists on the active tab and resume it /// with the outcome of the text editor. pub(super) fn post_editor_closed(&mut self, committed: bool) -> Task { @@ -555,6 +590,7 @@ impl OpenCADStudio { self.tabs.push(new_tab); let idx = self.tabs.len() - 1; self.active_tab = idx; + self.apply_bg_default(idx); idx }; @@ -1346,6 +1382,8 @@ impl OpenCADStudio { let new_tab = super::document::DocumentTab::new_drawing(self.tab_counter); self.tabs.push(new_tab); self.active_tab = self.tabs.len() - 1; + let idx = self.active_tab; + self.apply_bg_default(idx); self.sync_ribbon_layers(); self.sync_ribbon_styles(); // #21: reset ribbon Color / Linetype / Lineweight to the @@ -1397,6 +1435,7 @@ impl OpenCADStudio { self.tab_counter += 1; self.tabs[0] = super::document::DocumentTab::new_drawing(self.tab_counter); self.active_tab = 0; + self.apply_bg_default(0); } else { self.tabs.remove(idx); if self.active_tab >= self.tabs.len() { @@ -6474,6 +6513,7 @@ impl OpenCADStudio { self.tabs[0] = super::document::DocumentTab::new_drawing(self.tab_counter); self.active_tab = 0; + self.apply_bg_default(0); } else { self.tabs.remove(idx); if self.active_tab >= self.tabs.len() { @@ -9591,3 +9631,19 @@ fn parse_plot_scale(s: &str) -> (f64, f64) { } (1.0, 1.0) } + +/// Convert an internal `[r,g,b,a]` colour (0.0–1.0) to a persisted 0–255 RGB +/// triplet, dropping alpha (backgrounds are always opaque). +fn f4_to_u3([r, g, b, _]: [f32; 4]) -> [u8; 3] { + [ + (r * 255.0).round().clamp(0.0, 255.0) as u8, + (g * 255.0).round().clamp(0.0, 255.0) as u8, + (b * 255.0).round().clamp(0.0, 255.0) as u8, + ] +} + +/// Inverse of [`f4_to_u3`]: a persisted 0–255 RGB triplet back to an opaque +/// `[r,g,b,a]` colour. +fn u3_to_f4([r, g, b]: [u8; 3]) -> [f32; 4] { + [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0] +}