forked from 1b-agent/opencadstudio-upstream
feat(model-space): theme-aware model space canvas and CAD selection/grip sysvars
Model space appearance now follows the active UI theme instead of a hardcoded dark background. Canvas mode is user-selectable (Match Theme / Classic CAD Dark / Custom RGB), with matching controls for the paper sheet and desk surround backgrounds and grid opacity. Implements industry-standard CAD system variables, all settable via command name, SETVAR, and the Options dialog: - COLORTHEME (0/1 dark/light UI scheme, preserves custom palettes) - COLORSCHEME (interactive named-theme selection) - BACKGROUND (THEME/CLASSIC/DEFAULT/DESK presets, RGB triplets) - SELECTIONAREA, SELECTIONAREAOPACITY (translucent marquee fill) - WINDOWSAREACOLOR/WINDOWAREACOLOR, CROSSINGAREACOLOR (ACI or theme) - SELECTIONEFFECT, SELECTIONEFFECTCOLOR (GPU highlight pass, driven by selection_generation invalidation — no retessellation) - SELECTIONPREVIEW (bit 0 = idle rollover, bit 1 = in-command hover) - GRIPSIZE, GRIPCOLOR, GRIPHOT, GRIPHOVER (grip overlay styling)
This commit is contained in:
1 parent
89b1a22850
commit
688785a6cf
18 files changed
+1949
-316
No files matched your search
+73
-40
@@ -243,7 +243,7 @@ impl OpenCADStudio {
|
||||
use crate::command::ValuePromptCommand;
|
||||
let c = ValuePromptCommand::new(
|
||||
"BACKGROUND",
|
||||
"BACKGROUND colour [Default/Black/DarkGray/Gray/LightGray/White] or R G B (0–255):",
|
||||
"BACKGROUND colour [Theme/Classic/Default/Black/DarkGray/Gray/LightGray/White] or R G B (0–255):",
|
||||
);
|
||||
self.command_line.push_info(&c.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
@@ -251,56 +251,78 @@ impl OpenCADStudio {
|
||||
cmd if cmd.starts_with("BACKGROUND ") => {
|
||||
let args = cmd.split_whitespace().skip(1).collect::<Vec<_>>();
|
||||
let is_paper = self.tabs[i].scene.current_layout != "Model";
|
||||
if args
|
||||
.first()
|
||||
.map(|s| s.eq_ignore_ascii_case("DEFAULT") || s.eq_ignore_ascii_case("RESET"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let first_arg = args.first().map(|s| s.to_uppercase()).unwrap_or_default();
|
||||
if first_arg == "THEME" || first_arg == "AUTO" {
|
||||
self.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
self.command_line
|
||||
.push_output(crate::t!("Model space background set to Match Theme.").as_ref());
|
||||
} else if first_arg == "CLASSIC" {
|
||||
self.model_space.mode = crate::app::config::ModelSpaceMode::ClassicDark;
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
self.command_line
|
||||
.push_output(crate::t!("Model space background set to Classic CAD Dark.").as_ref());
|
||||
} else if first_arg == "DEFAULT" || first_arg == "RESET" {
|
||||
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;
|
||||
self.model_space.custom_paper_bg = None;
|
||||
self.paper_bg_input.clear();
|
||||
} else {
|
||||
self.tabs[i].bg_color = None;
|
||||
self.tabs[i].scene.bg_color = [33.0 / 255.0, 40.0 / 255.0, 48.0 / 255.0, 1.0];
|
||||
self.default_bg_color = None;
|
||||
self.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
|
||||
self.model_space.custom_bg = None;
|
||||
self.model_bg_input.clear();
|
||||
}
|
||||
// Wire colour adaptation (`adapt_to_bg`) reads the bg
|
||||
// at tessellation time, so the cached wires need to
|
||||
// refresh — otherwise a light→dark bg flip leaves
|
||||
// black lines invisible against the new bg. Meshes
|
||||
// bake colour into per-vertex GPU buffers at upload
|
||||
// time; `recolor_meshes` rewrites the CPU side so
|
||||
// the next epoch-driven re-upload picks up the new
|
||||
// colour.
|
||||
self.tabs[i].scene.recolor_meshes();
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
self.command_line
|
||||
.push_output(crate::t!("Background reset to default.").as_ref());
|
||||
} else if let Some(rgba) = parse_background_color(&args) {
|
||||
if is_paper {
|
||||
self.tabs[i].paper_bg_color = Some(rgba);
|
||||
self.tabs[i].scene.paper_bg_color = rgba;
|
||||
self.default_paper_bg_color = Some(rgba);
|
||||
} else if first_arg == "DESK" {
|
||||
let sub_args = &args[1..];
|
||||
let sub_first = sub_args.first().map(|s| s.to_uppercase()).unwrap_or_default();
|
||||
if sub_first == "DEFAULT" || sub_first == "RESET" {
|
||||
self.model_space.custom_desk_bg = None;
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
self.command_line
|
||||
.push_output(crate::t!("Paper desk surround background reset to default.").as_ref());
|
||||
} else if let Some(rgba) = parse_background_color(sub_args) {
|
||||
let [r, g, b, _] = rgba;
|
||||
let rgb = [(r * 255.0).round() as u8, (g * 255.0).round() as u8, (b * 255.0).round() as u8];
|
||||
self.model_space.custom_desk_bg = Some(rgb);
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"Desk surround background: rgb({}, {}, {})",
|
||||
rgb[0],
|
||||
rgb[1],
|
||||
rgb[2]
|
||||
).as_ref());
|
||||
} else {
|
||||
self.tabs[i].bg_color = Some(rgba);
|
||||
self.tabs[i].scene.bg_color = rgba;
|
||||
self.default_bg_color = Some(rgba);
|
||||
self.command_line.push_info("Usage: BACKGROUND DESK <r> <g> <b> | DEFAULT");
|
||||
}
|
||||
self.tabs[i].scene.recolor_meshes();
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
} else if let Some(rgba) = parse_background_color(&args) {
|
||||
let [r, g, b, _] = rgba;
|
||||
let rgb = [(r * 255.0).round() as u8, (g * 255.0).round() as u8, (b * 255.0).round() as u8];
|
||||
if is_paper {
|
||||
self.model_space.custom_paper_bg = Some(rgb);
|
||||
self.paper_bg_input = crate::app::config::rgb_to_hex(rgb);
|
||||
} else {
|
||||
self.model_space.mode = crate::app::config::ModelSpaceMode::Custom;
|
||||
self.model_space.custom_bg = Some(rgb);
|
||||
self.model_bg_input = crate::app::config::rgb_to_hex(rgb);
|
||||
}
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
self.command_line.push_output(crate::tf!(
|
||||
"Background: rgb({}, {}, {})",
|
||||
(r * 255.0).round() as u8,
|
||||
(g * 255.0).round() as u8,
|
||||
(b * 255.0).round() as u8
|
||||
rgb[0],
|
||||
rgb[1],
|
||||
rgb[2]
|
||||
).as_ref());
|
||||
// Persisted centrally after this message via
|
||||
// `persist_settings_if_changed()`.
|
||||
} else {
|
||||
self.command_line.push_info(
|
||||
"Usage: BACKGROUND <r> <g> <b> (0–255) | DEFAULT|BLACK|DARKGRAY|GRAY|LIGHTGRAY|WHITE",
|
||||
"Usage: BACKGROUND <r> <g> <b> (0–255) | THEME|CLASSIC|DEFAULT|DESK|BLACK|DARKGRAY|GRAY|LIGHTGRAY|WHITE",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -366,9 +388,20 @@ impl OpenCADStudio {
|
||||
/// Returns `None` if the arguments don't match either form.
|
||||
fn parse_background_color(args: &[&str]) -> Option<[f32; 4]> {
|
||||
let to_rgba = |[r, g, b]: [u8; 3]| [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0];
|
||||
// Single token: a named preset.
|
||||
// Single token: a named preset or comma-separated "r,g,b".
|
||||
if args.len() == 1 {
|
||||
let preset = match args[0].to_ascii_uppercase().as_str() {
|
||||
let text = args[0].trim();
|
||||
let parts: Vec<&str> = text.split(',').collect();
|
||||
if parts.len() == 3 {
|
||||
if let (Ok(r), Ok(g), Ok(b)) = (
|
||||
parts[0].trim().parse::<u8>(),
|
||||
parts[1].trim().parse::<u8>(),
|
||||
parts[2].trim().parse::<u8>(),
|
||||
) {
|
||||
return Some(to_rgba([r, g, b]));
|
||||
}
|
||||
}
|
||||
let preset = match text.to_ascii_uppercase().as_str() {
|
||||
"WHITE" => [255, 255, 255],
|
||||
"BLACK" => [0, 0, 0],
|
||||
"GRAY" | "GREY" => [128, 128, 128],
|
||||
|
||||
@@ -534,6 +534,19 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"SKPOLY",
|
||||
"SKTOLERANCE",
|
||||
"CLIPROMPTLINES",
|
||||
"COLORTHEME",
|
||||
"SELECTIONAREA",
|
||||
"SELECTIONAREAOPACITY",
|
||||
"SELECTIONEFFECT",
|
||||
"SELECTIONEFFECTCOLOR",
|
||||
"WINDOWSAREACOLOR",
|
||||
"WINDOWAREACOLOR",
|
||||
"CROSSINGAREACOLOR",
|
||||
"SELECTIONPREVIEW",
|
||||
"GRIPSIZE",
|
||||
"GRIPCOLOR",
|
||||
"GRIPHOT",
|
||||
"GRIPHOVER",
|
||||
// Reset selected entities' overrides to follow their layer.
|
||||
"SETBYLAYER",
|
||||
// Remove duplicate objects; set drawing base point; audit integrity;
|
||||
|
||||
@@ -888,6 +888,19 @@ impl OpenCADStudio {
|
||||
| "CENTERCROSSGAP"
|
||||
| "CENTERMARKEXE"
|
||||
| "DIMCONTINUEMODE"
|
||||
| "COLORTHEME"
|
||||
| "SELECTIONAREA"
|
||||
| "SELECTIONAREAOPACITY"
|
||||
| "SELECTIONEFFECT"
|
||||
| "SELECTIONEFFECTCOLOR"
|
||||
| "WINDOWSAREACOLOR"
|
||||
| "WINDOWAREACOLOR"
|
||||
| "CROSSINGAREACOLOR"
|
||||
| "SELECTIONPREVIEW"
|
||||
| "GRIPSIZE"
|
||||
| "GRIPCOLOR"
|
||||
| "GRIPHOT"
|
||||
| "GRIPHOVER"
|
||||
) =>
|
||||
{
|
||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||
@@ -910,7 +923,7 @@ impl OpenCADStudio {
|
||||
let value = it.next().map(|s| s.trim().to_string());
|
||||
if name.is_empty() || name == "?" {
|
||||
self.command_line.push_info(
|
||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER | CLAYER CELTYPE TEXTSTYLE (read-only)",
|
||||
);
|
||||
} else {
|
||||
let frame_kind = crate::scene::frame::kind_for_name(&name);
|
||||
@@ -1251,6 +1264,171 @@ impl OpenCADStudio {
|
||||
false,
|
||||
)),
|
||||
},
|
||||
"COLORTHEME" => match &value {
|
||||
Some(v) => match v.as_str() {
|
||||
"0" => {
|
||||
if self.ui_theme.name == "Custom" {
|
||||
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||
}
|
||||
self.ui_theme.name = iced::Theme::Dark.to_string();
|
||||
self.ui_theme.palette =
|
||||
crate::app::config::UiThemePalette::from_iced(iced::Theme::Dark.seed());
|
||||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||
self.active_theme = iced::Theme::Dark;
|
||||
self.sync_model_space_theme(true);
|
||||
Ok(("COLORTHEME = 0 (Dark)".to_string(), true))
|
||||
}
|
||||
"1" => {
|
||||
if self.ui_theme.name == "Custom" {
|
||||
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||
}
|
||||
self.ui_theme.name = iced::Theme::Light.to_string();
|
||||
self.ui_theme.palette =
|
||||
crate::app::config::UiThemePalette::from_iced(iced::Theme::Light.seed());
|
||||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||
self.active_theme = iced::Theme::Light;
|
||||
self.sync_model_space_theme(true);
|
||||
Ok(("COLORTHEME = 1 (Light)".to_string(), true))
|
||||
}
|
||||
_ => Err("SETVAR: 0 (Dark) or 1 (Light) required.".into()),
|
||||
},
|
||||
None => {
|
||||
let bg = self.ui_theme.palette.background;
|
||||
let lum = 0.299 * (bg[0] as f32) + 0.587 * (bg[1] as f32) + 0.114 * (bg[2] as f32);
|
||||
let code = if lum > 128.0 { 1 } else { 0 };
|
||||
Ok((format!("COLORTHEME = {code} ({})", self.ui_theme.name), false))
|
||||
}
|
||||
},
|
||||
"SELECTIONAREA" => match &value {
|
||||
Some(v) => match v.as_str() {
|
||||
"0" => {
|
||||
self.model_space.selection_area = false;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok(("SELECTIONAREA = 0".to_string(), true))
|
||||
}
|
||||
"1" => {
|
||||
self.model_space.selection_area = true;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok(("SELECTIONAREA = 1".to_string(), true))
|
||||
}
|
||||
_ => Err("SETVAR: 0 or 1 required.".into()),
|
||||
},
|
||||
None => Ok((format!("SELECTIONAREA = {}", if self.model_space.selection_area { 1 } else { 0 }), false)),
|
||||
},
|
||||
"SELECTIONAREAOPACITY" => match &value {
|
||||
Some(v) => match v.parse::<u8>() {
|
||||
Ok(op) if op <= 100 => {
|
||||
self.model_space.selection_opacity = op;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok((format!("SELECTIONAREAOPACITY = {op}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 0 to 100 required.".into()),
|
||||
},
|
||||
None => Ok((format!("SELECTIONAREAOPACITY = {}", self.model_space.selection_opacity), false)),
|
||||
},
|
||||
"SELECTIONEFFECT" => match &value {
|
||||
Some(v) => match v.as_str() {
|
||||
"0" => {
|
||||
self.model_space.selection_effect = false;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok(("SELECTIONEFFECT = 0".to_string(), true))
|
||||
}
|
||||
"1" => {
|
||||
self.model_space.selection_effect = true;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok(("SELECTIONEFFECT = 1".to_string(), true))
|
||||
}
|
||||
_ => Err("SETVAR: 0 or 1 required.".into()),
|
||||
},
|
||||
None => Ok((format!("SELECTIONEFFECT = {}", if self.model_space.selection_effect { 1 } else { 0 }), false)),
|
||||
},
|
||||
"SELECTIONEFFECTCOLOR" => match &value {
|
||||
Some(v) => match v.parse::<u8>() {
|
||||
Ok(color) => {
|
||||
self.model_space.selection_highlight_color = color;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok((format!("SELECTIONEFFECTCOLOR = {color}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||
},
|
||||
None => Ok((format!("SELECTIONEFFECTCOLOR = {}", self.model_space.selection_highlight_color), false)),
|
||||
},
|
||||
"WINDOWSAREACOLOR" | "WINDOWAREACOLOR" => match &value {
|
||||
Some(v) => match v.parse::<u8>() {
|
||||
Ok(color) => {
|
||||
self.model_space.selection_window_color = color;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok((format!("WINDOWSAREACOLOR = {color}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||
},
|
||||
None => Ok((format!("WINDOWSAREACOLOR = {}", self.model_space.selection_window_color), false)),
|
||||
},
|
||||
"CROSSINGAREACOLOR" => match &value {
|
||||
Some(v) => match v.parse::<u8>() {
|
||||
Ok(color) => {
|
||||
self.model_space.selection_crossing_color = color;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok((format!("CROSSINGAREACOLOR = {color}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||
},
|
||||
None => Ok((format!("CROSSINGAREACOLOR = {}", self.model_space.selection_crossing_color), false)),
|
||||
},
|
||||
"SELECTIONPREVIEW" => match &value {
|
||||
Some(v) => match v.parse::<u8>() {
|
||||
Ok(mode @ 0..=3) => {
|
||||
self.model_space.selection_preview = mode;
|
||||
Ok((format!("SELECTIONPREVIEW = {mode}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 0 to 3 required.".into()),
|
||||
},
|
||||
None => Ok((format!("SELECTIONPREVIEW = {}", self.model_space.selection_preview), false)),
|
||||
},
|
||||
"GRIPSIZE" => match &value {
|
||||
Some(v) => match v.parse::<u8>() {
|
||||
Ok(size @ 1..=25) => {
|
||||
self.model_space.grip_size = size;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok((format!("GRIPSIZE = {size}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 1 to 25 required.".into()),
|
||||
},
|
||||
None => Ok((format!("GRIPSIZE = {}", self.model_space.grip_size), false)),
|
||||
},
|
||||
"GRIPCOLOR" => match &value {
|
||||
Some(v) => match v.parse::<u8>() {
|
||||
Ok(color) => {
|
||||
self.model_space.grip_color = color;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok((format!("GRIPCOLOR = {color}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||
},
|
||||
None => Ok((format!("GRIPCOLOR = {}", self.model_space.grip_color), false)),
|
||||
},
|
||||
"GRIPHOT" => match &value {
|
||||
Some(v) => match v.parse::<u8>() {
|
||||
Ok(color) => {
|
||||
self.model_space.grip_hot = color;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok((format!("GRIPHOT = {color}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||
},
|
||||
None => Ok((format!("GRIPHOT = {}", self.model_space.grip_hot), false)),
|
||||
},
|
||||
"GRIPHOVER" => match &value {
|
||||
Some(v) => match v.parse::<u8>() {
|
||||
Ok(color) => {
|
||||
self.model_space.grip_hover = color;
|
||||
self.sync_model_space_theme(false);
|
||||
Ok((format!("GRIPHOVER = {color}"), true))
|
||||
}
|
||||
_ => Err("SETVAR: integer from 0 (Theme) to 255 required.".into()),
|
||||
},
|
||||
None => Ok((format!("GRIPHOVER = {}", self.model_space.grip_hover), false)),
|
||||
},
|
||||
"SNAPANG" => match &value {
|
||||
Some(v) => match v.parse::<f32>() {
|
||||
Ok(angle) if angle.is_finite() => {
|
||||
@@ -1872,6 +2050,19 @@ impl OpenCADStudio {
|
||||
| "CURSORTYPE"
|
||||
| "SNAPANG"
|
||||
| "CLIPROMPTLINES"
|
||||
| "COLORTHEME"
|
||||
| "SELECTIONAREA"
|
||||
| "SELECTIONAREAOPACITY"
|
||||
| "SELECTIONEFFECT"
|
||||
| "SELECTIONEFFECTCOLOR"
|
||||
| "WINDOWSAREACOLOR"
|
||||
| "WINDOWAREACOLOR"
|
||||
| "CROSSINGAREACOLOR"
|
||||
| "SELECTIONPREVIEW"
|
||||
| "GRIPSIZE"
|
||||
| "GRIPCOLOR"
|
||||
| "GRIPHOT"
|
||||
| "GRIPHOVER"
|
||||
) {
|
||||
self.persist_settings_if_changed();
|
||||
} else {
|
||||
@@ -2671,3 +2862,230 @@ fn rename_symbol(doc: &mut acadrust::CadDocument, ty: &str, old: &str, new: &str
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::OpenCADStudio;
|
||||
|
||||
fn fresh_app() -> OpenCADStudio {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
app
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cad_selection_and_model_space_sysvars() {
|
||||
let mut app = fresh_app();
|
||||
|
||||
// COLORTHEME 1 (Light)
|
||||
let _ = app.run_command_line("SETVAR COLORTHEME 1");
|
||||
assert_eq!(app.active_theme, iced::Theme::Light);
|
||||
assert_eq!(app.ui_theme.name, "Light");
|
||||
|
||||
// COLORTHEME 0 (Dark)
|
||||
let _ = app.run_command_line("SETVAR COLORTHEME 0");
|
||||
assert_eq!(app.active_theme, iced::Theme::Dark);
|
||||
assert_eq!(app.ui_theme.name, "Dark");
|
||||
|
||||
// Direct command name: COLORTHEME 1
|
||||
let _ = app.run_command_line("COLORTHEME 1");
|
||||
assert_eq!(app.active_theme, iced::Theme::Light);
|
||||
|
||||
// SELECTIONAREA toggle
|
||||
let _ = app.run_command_line("SETVAR SELECTIONAREA 0");
|
||||
assert!(!app.model_space.selection_area);
|
||||
let _ = app.run_command_line("SELECTIONAREA 1");
|
||||
assert!(app.model_space.selection_area);
|
||||
|
||||
// SELECTIONAREAOPACITY
|
||||
let _ = app.run_command_line("SETVAR SELECTIONAREAOPACITY 45");
|
||||
assert_eq!(app.model_space.selection_opacity, 45);
|
||||
let _ = app.run_command_line("SELECTIONAREAOPACITY 80");
|
||||
assert_eq!(app.model_space.selection_opacity, 80);
|
||||
|
||||
// SELECTIONEFFECT
|
||||
let _ = app.run_command_line("SETVAR SELECTIONEFFECT 0");
|
||||
assert!(!app.model_space.selection_effect);
|
||||
assert!(!app.tabs[app.active_tab].scene.selection_effect);
|
||||
let _ = app.run_command_line("SELECTIONEFFECT 1");
|
||||
assert!(app.model_space.selection_effect);
|
||||
assert!(app.tabs[app.active_tab].scene.selection_effect);
|
||||
|
||||
// SELECTIONEFFECTCOLOR
|
||||
let _ = app.run_command_line("SETVAR SELECTIONEFFECTCOLOR 4");
|
||||
assert_eq!(app.model_space.selection_highlight_color, 4);
|
||||
assert_eq!(
|
||||
app.tabs[app.active_tab].scene.selection_color,
|
||||
app.model_space.resolve_selection_color()
|
||||
);
|
||||
|
||||
// WINDOWSAREACOLOR & CROSSINGAREACOLOR
|
||||
let _ = app.run_command_line("SETVAR WINDOWSAREACOLOR 5");
|
||||
assert_eq!(app.model_space.selection_window_color, 5);
|
||||
let _ = app.run_command_line("WINDOWSAREACOLOR 1");
|
||||
assert_eq!(app.model_space.selection_window_color, 1);
|
||||
|
||||
let _ = app.run_command_line("SETVAR CROSSINGAREACOLOR 3");
|
||||
assert_eq!(app.model_space.selection_crossing_color, 3);
|
||||
let _ = app.run_command_line("CROSSINGAREACOLOR 2");
|
||||
assert_eq!(app.model_space.selection_crossing_color, 2);
|
||||
|
||||
// SELECTIONPREVIEW
|
||||
let _ = app.run_command_line("SETVAR SELECTIONPREVIEW 2");
|
||||
assert_eq!(app.model_space.selection_preview, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_background_command_modes() {
|
||||
let mut app = fresh_app();
|
||||
let i = app.active_tab;
|
||||
|
||||
// BACKGROUND CLASSIC
|
||||
let _ = app.run_command_line("BACKGROUND CLASSIC");
|
||||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::ClassicDark);
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.bg_color,
|
||||
[33.0 / 255.0, 40.0 / 255.0, 48.0 / 255.0, 1.0]
|
||||
);
|
||||
|
||||
// Switch to light theme and set BACKGROUND THEME
|
||||
let _ = app.run_command_line("COLORTHEME 1");
|
||||
let _ = app.run_command_line("BACKGROUND THEME");
|
||||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||||
assert_eq!(app.tabs[i].scene.bg_color, [1.0, 1.0, 1.0, 1.0]);
|
||||
|
||||
// BACKGROUND RGB custom
|
||||
let _ = app.run_command_line("BACKGROUND 50 60 70");
|
||||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::Custom);
|
||||
assert_eq!(app.model_space.custom_bg, Some([50, 60, 70]));
|
||||
assert!((app.tabs[i].scene.bg_color[0] - 50.0 / 255.0).abs() < 1e-4);
|
||||
assert!((app.tabs[i].scene.bg_color[1] - 60.0 / 255.0).abs() < 1e-4);
|
||||
assert!((app.tabs[i].scene.bg_color[2] - 70.0 / 255.0).abs() < 1e-4);
|
||||
|
||||
// BACKGROUND DEFAULT restores MatchTheme
|
||||
let _ = app.run_command_line("BACKGROUND DEFAULT");
|
||||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||||
assert_eq!(app.model_space.custom_bg, None);
|
||||
|
||||
// BACKGROUND DESK
|
||||
let _ = app.run_command_line("BACKGROUND DESK 80 85 90");
|
||||
assert_eq!(app.model_space.custom_desk_bg, Some([80, 85, 90]));
|
||||
let _ = app.run_command_line("BACKGROUND DESK DEFAULT");
|
||||
assert_eq!(app.model_space.custom_desk_bg, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_colorscheme_command() {
|
||||
let mut app = fresh_app();
|
||||
app.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
|
||||
let i = app.active_tab;
|
||||
|
||||
// Bare COLORSCHEME launches interactive ValuePromptCommand
|
||||
let _ = app.run_command_line("COLORSCHEME");
|
||||
assert!(app.tabs[i].active_cmd.is_some());
|
||||
assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "COLORSCHEME");
|
||||
|
||||
// Cancel the interactive command
|
||||
app.tabs[i].active_cmd = None;
|
||||
|
||||
// COLORSCHEME LIGHT sets Light theme and synchronizes model space
|
||||
let _ = app.run_command_line("COLORSCHEME LIGHT");
|
||||
assert_eq!(app.active_theme, iced::Theme::Light);
|
||||
assert_eq!(app.tabs[i].scene.bg_color, [1.0, 1.0, 1.0, 1.0]);
|
||||
|
||||
// Case-insensitive and spaces: "colorscheme tokyo night"
|
||||
let _ = app.run_command_line("colorscheme tokyo night");
|
||||
assert_eq!(app.active_theme, iced::Theme::TokyoNight);
|
||||
|
||||
// COLORSCHEME rejects 1 and 0 (which belong to COLORTHEME)
|
||||
let _ = app.run_command_line("colorscheme 1");
|
||||
assert_ne!(app.active_theme, iced::Theme::Light); // stays TokyoNight
|
||||
|
||||
// COLORTHEME accepts 1 and 0
|
||||
let _ = app.run_command_line("COLORTHEME 1");
|
||||
assert_eq!(app.active_theme, iced::Theme::Light);
|
||||
let _ = app.run_command_line("COLORTHEME 0");
|
||||
assert_eq!(app.active_theme, iced::Theme::Dark);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reviewer_feedback_fixes() {
|
||||
let mut app = fresh_app();
|
||||
|
||||
// 1. COLORTHEME query returns UI theme scheme regardless of canvas background
|
||||
let _ = app.run_command_line("COLORTHEME 0");
|
||||
// Set canvas background to bright white in custom mode
|
||||
let _ = app.run_command_line("BACKGROUND 255 255 255");
|
||||
let _ = app.run_command_line("SETVAR COLORTHEME");
|
||||
let output = app.command_line.history_plain_text();
|
||||
assert!(output.contains("<0 (Dark)>"), "UI is dark, so query must return 0 despite white canvas: {output}");
|
||||
|
||||
let _ = app.run_command_line("COLORTHEME 1");
|
||||
let _ = app.run_command_line("SETVAR COLORTHEME");
|
||||
let output = app.command_line.history_plain_text();
|
||||
assert!(output.contains("<1 (Light)>"), "UI is light, so query must return 1: {output}");
|
||||
|
||||
// 2. Custom theme safety when COLORTHEME 0/1 or COLORSCHEME runs
|
||||
app.ui_theme.name = "Custom".to_string();
|
||||
app.ui_theme.palette.background = [12, 34, 56];
|
||||
let _ = app.run_command_line("COLORTHEME 0");
|
||||
assert_eq!(app.saved_custom_palette.map(|p| p.background), Some([12, 34, 56]));
|
||||
|
||||
app.saved_custom_palette = None;
|
||||
app.ui_theme.name = "Custom".to_string();
|
||||
app.ui_theme.palette.background = [78, 90, 12];
|
||||
let _ = app.run_command_line("COLORSCHEME DRACULA");
|
||||
assert_eq!(app.saved_custom_palette.map(|p| p.background), Some([78, 90, 12]));
|
||||
|
||||
// 3. ModelSpaceBgChanged empty reverts mode to MatchTheme
|
||||
let _ = app.update(crate::app::Message::ModelSpaceBgChanged("".to_string()));
|
||||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||||
assert_eq!(app.model_space.custom_bg, None);
|
||||
|
||||
// 4. Split restore defaults: Display vs Selection
|
||||
app.model_space.mode = crate::app::config::ModelSpaceMode::Custom;
|
||||
app.model_space.custom_bg = Some([10, 20, 30]);
|
||||
app.model_space.selection_opacity = 99;
|
||||
let _ = app.update(crate::app::Message::RestoreModelSpaceDisplayDefaults);
|
||||
assert_eq!(app.model_space.mode, crate::app::config::ModelSpaceMode::MatchTheme);
|
||||
assert_eq!(app.model_space.selection_opacity, 99, "Selection visual must not be wiped by display restore");
|
||||
|
||||
let _ = app.update(crate::app::Message::RestoreSelectionVisualDefaults);
|
||||
assert_eq!(app.model_space.selection_opacity, 12, "Selection restore resets selection visual defaults");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_grip_and_desk_surround_sysvars() {
|
||||
let mut app = fresh_app();
|
||||
|
||||
// 1. Desk surround background message
|
||||
let _ = app.update(crate::app::Message::DeskSpaceBgChanged("#1a2b3c".to_string()));
|
||||
assert_eq!(app.model_space.custom_desk_bg, Some([0x1a, 0x2b, 0x3c]));
|
||||
assert_eq!(app.desk_bg_input, "#1a2b3c");
|
||||
let _ = app.update(crate::app::Message::DeskSpaceBgChanged("".to_string()));
|
||||
assert_eq!(app.model_space.custom_desk_bg, None);
|
||||
|
||||
// 2. GRIPSIZE, GRIPCOLOR, GRIPHOT, GRIPHOVER via command line & SETVAR
|
||||
let _ = app.run_command_line("GRIPSIZE 8");
|
||||
assert_eq!(app.model_space.grip_size, 8);
|
||||
let _ = app.run_command_line("GRIPCOLOR 4");
|
||||
assert_eq!(app.model_space.grip_color, 4);
|
||||
let _ = app.run_command_line("GRIPHOT 1");
|
||||
assert_eq!(app.model_space.grip_hot, 1);
|
||||
let _ = app.run_command_line("GRIPHOVER 2");
|
||||
assert_eq!(app.model_space.grip_hover, 2);
|
||||
|
||||
// Direct messages
|
||||
let _ = app.update(crate::app::Message::GripSizeChanged(12));
|
||||
assert_eq!(app.model_space.grip_size, 12);
|
||||
let _ = app.update(crate::app::Message::GripColorChanged(5));
|
||||
assert_eq!(app.model_space.grip_color, 5);
|
||||
|
||||
// Restore defaults resets grips
|
||||
let _ = app.update(crate::app::Message::RestoreSelectionVisualDefaults);
|
||||
assert_eq!(app.model_space.grip_size, 5);
|
||||
assert_eq!(app.model_space.grip_color, 0);
|
||||
assert_eq!(app.model_space.grip_hot, 0);
|
||||
assert_eq!(app.model_space.grip_hover, 0);
|
||||
}
|
||||
}
|
||||
+61
-45
@@ -375,56 +375,72 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
// ── Color Scheme / Theme selector ─────────────────────────────
|
||||
cmd if cmd == "COLORSCHEME" || cmd.starts_with("COLORSCHEME ") => {
|
||||
use iced::Theme;
|
||||
let sub = cmd
|
||||
.split_once(' ')
|
||||
.map(|(_, r)| r.trim())
|
||||
.unwrap_or("")
|
||||
.to_uppercase();
|
||||
// Map name to Theme variant.
|
||||
let theme: Option<Theme> = match sub.as_str() {
|
||||
"DARK" => Some(Theme::Dark),
|
||||
"LIGHT" => Some(Theme::Light),
|
||||
"DRACULA" => Some(Theme::Dracula),
|
||||
"NORD" => Some(Theme::Nord),
|
||||
"SOLARIZED_LIGHT" | "SOLARIZEDLIGHT" => Some(Theme::SolarizedLight),
|
||||
"SOLARIZED_DARK" | "SOLARIZEDDARK" => Some(Theme::SolarizedDark),
|
||||
"GRUVBOX_LIGHT" | "GRUVBOXLIGHT" => Some(Theme::GruvboxLight),
|
||||
"GRUVBOX_DARK" | "GRUVBOXDARK" => Some(Theme::GruvboxDark),
|
||||
"TOKYONIGHT" | "TOKYO_NIGHT" => Some(Theme::TokyoNight),
|
||||
"TOKYONIGHTSTORM" | "TOKYO_NIGHT_STORM" => Some(Theme::TokyoNightStorm),
|
||||
"TOKYONIGHTLIGHT" | "TOKYO_NIGHT_LIGHT" => Some(Theme::TokyoNightLight),
|
||||
"KANAGAWAWAVE" | "KANAGAWA_WAVE" => Some(Theme::KanagawaWave),
|
||||
"KANAGAWADRAGON" | "KANAGAWA_DRAGON" => Some(Theme::KanagawaDragon),
|
||||
"KANAGAWALOTUS" | "KANAGAWA_LOTUS" => Some(Theme::KanagawaLotus),
|
||||
"MOONFLY" => Some(Theme::Moonfly),
|
||||
"NIGHTFLY" => Some(Theme::Nightfly),
|
||||
"OXOCARBON" => Some(Theme::Oxocarbon),
|
||||
"FERRA" => Some(Theme::Ferra),
|
||||
"" | "LIST" | "?" => {
|
||||
self.command_line.push_output(
|
||||
"Available themes: DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
|
||||
GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \
|
||||
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA"
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
_ => {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"COLORSCHEME: unknown theme '{}'. Type COLORSCHEME LIST for options.",
|
||||
sub
|
||||
).as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
};
|
||||
cmd if cmd.eq_ignore_ascii_case("COLORSCHEME") => {
|
||||
use crate::command::ValuePromptCommand;
|
||||
let c = ValuePromptCommand::new(
|
||||
"COLORSCHEME",
|
||||
"COLORSCHEME Enter theme name (or ? to list):",
|
||||
);
|
||||
self.command_line.push_output(
|
||||
"Available themes: DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
|
||||
GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \
|
||||
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA",
|
||||
);
|
||||
self.command_line.push_info(&format!(
|
||||
"Current color scheme: <{}>",
|
||||
self.ui_theme.name
|
||||
));
|
||||
self.command_line.push_info(&c.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||
return Some(Task::none());
|
||||
}
|
||||
cmd if cmd.to_ascii_uppercase().starts_with("COLORSCHEME ") => {
|
||||
let sub = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or("");
|
||||
let upper_sub = sub.to_ascii_uppercase();
|
||||
if upper_sub.is_empty() {
|
||||
self.command_line.push_output(&format!(
|
||||
"Current color scheme: <{}>",
|
||||
self.ui_theme.name
|
||||
));
|
||||
return Some(Task::none());
|
||||
}
|
||||
if upper_sub == "LIST" || upper_sub == "?" {
|
||||
self.command_line.push_output(
|
||||
"Available themes: DARK LIGHT DRACULA NORD SOLARIZED_LIGHT SOLARIZED_DARK \
|
||||
GRUVBOX_LIGHT GRUVBOX_DARK TOKYONIGHT TOKYONIGHTSTORM TOKYONIGHTLIGHT \
|
||||
KANAGAWAWAVE KANAGAWADRAGON KANAGAWALOTUS MOONFLY NIGHTFLY OXOCARBON FERRA",
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
if sub == "0" || sub == "1" {
|
||||
self.command_line.push_error(
|
||||
"COLORSCHEME: 0 and 1 are COLORTHEME options. Enter a theme name (e.g. DARK, LIGHT, DRACULA) or use COLORTHEME.",
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
let theme = crate::app::config::parse_theme_name(sub);
|
||||
if let Some(t) = theme {
|
||||
if self.ui_theme.name == "Custom" {
|
||||
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||
}
|
||||
let name = format!("{:?}", t);
|
||||
self.ui_theme.name = t.to_string();
|
||||
self.ui_theme.palette =
|
||||
crate::app::config::UiThemePalette::from_iced(t.seed());
|
||||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||
self.active_theme = t.clone();
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
self.command_line
|
||||
.push_output(crate::tf!("Color scheme set to '{name}'.").as_ref());
|
||||
return Some(Task::done(Message::SetTheme(t)));
|
||||
return Some(Task::none());
|
||||
} else {
|
||||
self.command_line.push_error(crate::tf!(
|
||||
"COLORSCHEME: unknown theme '{}'. Type COLORSCHEME LIST for options.",
|
||||
sub
|
||||
).as_ref());
|
||||
return Some(Task::none());
|
||||
}
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
// ── Layout Manager GUI ─────────────────────────────────────────
|
||||
|
||||
Reference in new issue
Block a user