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
+55
-4
@@ -393,8 +393,8 @@ impl OpenCADStudio {
|
||||
pick_add: self.pick_add,
|
||||
pick_drag_rect: self.pick_drag_rect,
|
||||
quick_properties: self.quick_properties,
|
||||
bg_color: self.default_bg_color.map(f4_to_u3),
|
||||
paper_bg_color: self.default_paper_bg_color.map(f4_to_u3),
|
||||
bg_color: None,
|
||||
paper_bg_color: None,
|
||||
language: self.language,
|
||||
cliprompt_lines: crate::app::settings::clamp_clipromptlines(self.cliprompt_lines),
|
||||
block_mru: self.block_mru.clone(),
|
||||
@@ -455,8 +455,7 @@ impl OpenCADStudio {
|
||||
self.pick_add = s.pick_add;
|
||||
self.pick_drag_rect = s.pick_drag_rect;
|
||||
self.quick_properties = s.quick_properties;
|
||||
self.default_bg_color = s.bg_color.map(u3_to_f4);
|
||||
self.default_paper_bg_color = s.paper_bg_color.map(u3_to_f4);
|
||||
// Legacy settings.bg_color / paper_bg_color are superseded by model_space config.
|
||||
if crate::i18n::set_language(s.language).is_ok() {
|
||||
self.language = s.language;
|
||||
}
|
||||
@@ -775,6 +774,7 @@ impl OpenCADStudio {
|
||||
.map(|(key, command)| (key.clone(), command.clone()))
|
||||
.collect(),
|
||||
},
|
||||
model_space: self.model_space.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,6 +784,23 @@ impl OpenCADStudio {
|
||||
self.ui_theme = cfg.theme.clone();
|
||||
self.active_theme = self.ui_theme.to_iced();
|
||||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||
self.model_space = cfg.model_space;
|
||||
self.model_bg_input = self
|
||||
.model_space
|
||||
.custom_bg
|
||||
.map(crate::app::config::rgb_to_hex)
|
||||
.unwrap_or_default();
|
||||
self.paper_bg_input = self
|
||||
.model_space
|
||||
.custom_paper_bg
|
||||
.map(crate::app::config::rgb_to_hex)
|
||||
.unwrap_or_default();
|
||||
self.desk_bg_input = self
|
||||
.model_space
|
||||
.custom_desk_bg
|
||||
.map(crate::app::config::rgb_to_hex)
|
||||
.unwrap_or_default();
|
||||
self.sync_model_space_theme(false);
|
||||
self.recent_files = cfg
|
||||
.recent
|
||||
.files
|
||||
@@ -831,6 +848,40 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync the active theme and model space configuration into all open tabs
|
||||
/// and the application default background settings.
|
||||
pub(in crate::app) fn sync_model_space_theme(&mut self, geometry_changed: bool) {
|
||||
let model_bg = self.model_space.resolve_model_bg(&self.active_theme);
|
||||
let paper_bg = self.model_space.resolve_paper_bg();
|
||||
let sel_color = self.model_space.resolve_selection_color();
|
||||
let sel_effect = self.model_space.selection_effect;
|
||||
self.default_bg_color = Some(model_bg);
|
||||
self.default_paper_bg_color = Some(paper_bg);
|
||||
|
||||
for tab in &mut self.tabs {
|
||||
let old_bg = tab.scene.bg_color;
|
||||
let old_paper = tab.scene.paper_bg_color;
|
||||
let old_sel_color = tab.scene.selection_color;
|
||||
let old_sel_effect = tab.scene.selection_effect;
|
||||
tab.scene.bg_color = model_bg;
|
||||
tab.scene.paper_bg_color = paper_bg;
|
||||
tab.scene.selection_color = sel_color;
|
||||
tab.scene.selection_effect = sel_effect;
|
||||
tab.bg_color = Some(model_bg);
|
||||
tab.paper_bg_color = Some(paper_bg);
|
||||
|
||||
if old_sel_color != sel_color || old_sel_effect != sel_effect {
|
||||
tab.scene.selection_generation = tab.scene.selection_generation.wrapping_add(1);
|
||||
}
|
||||
|
||||
if geometry_changed || old_bg != model_bg || old_paper != paper_bg {
|
||||
tab.scene.recolor_meshes();
|
||||
tab.scene.bump_geometry();
|
||||
}
|
||||
tab.scene.request_refresh(crate::scene::ViewportRefreshScope::All);
|
||||
}
|
||||
}
|
||||
|
||||
/// Record that the one-time default-association prompt has been answered and
|
||||
/// flush it to disk, so the dialog never reappears on later launches.
|
||||
pub(in crate::app) fn mark_assoc_prompted(&mut self) {
|
||||
|
||||
+171
-12
@@ -726,12 +726,9 @@ impl OpenCADStudio {
|
||||
let size_label = format_size(size_bytes);
|
||||
self.command_line
|
||||
.push_info(crate::tf!("Opening \"{name}\" ({size_label})…").as_ref());
|
||||
let model_bg = self.default_bg_color.unwrap_or([
|
||||
33.0 / 255.0,
|
||||
40.0 / 255.0,
|
||||
48.0 / 255.0,
|
||||
1.0,
|
||||
]);
|
||||
let model_bg = self.default_bg_color.unwrap_or_else(|| {
|
||||
self.model_space.resolve_model_bg(&self.active_theme)
|
||||
});
|
||||
Task::perform(
|
||||
crate::io::open_path_with_phase(path, progress, model_bg),
|
||||
move |result| Message::FileOpened(open_id, result),
|
||||
@@ -5564,11 +5561,15 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
Message::SetTheme(theme) => {
|
||||
if self.ui_theme.name == "Custom" {
|
||||
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||
}
|
||||
self.ui_theme.name = theme.to_string();
|
||||
self.ui_theme.palette =
|
||||
crate::app::config::UiThemePalette::from_iced(theme.seed());
|
||||
self.theme_color_inputs = self.ui_theme.palette.hex_values();
|
||||
self.active_theme = theme;
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
@@ -5817,6 +5818,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
Message::OptionsThemeChanged(name) => {
|
||||
if self.ui_theme.name == "Custom" && name != "Custom" {
|
||||
self.saved_custom_palette = Some(self.ui_theme.palette);
|
||||
}
|
||||
self.ui_theme.name = name;
|
||||
if let Some(theme) =
|
||||
crate::app::config::builtin_theme(&self.ui_theme.name)
|
||||
@@ -5827,8 +5831,13 @@ impl OpenCADStudio {
|
||||
self.active_theme = theme;
|
||||
} else {
|
||||
self.ui_theme.name = "Custom".to_string();
|
||||
if let Some(saved) = self.saved_custom_palette {
|
||||
self.ui_theme.palette = saved;
|
||||
self.theme_color_inputs = saved.hex_values();
|
||||
}
|
||||
self.active_theme = self.ui_theme.to_iced();
|
||||
}
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
@@ -5841,11 +5850,164 @@ impl OpenCADStudio {
|
||||
if self.ui_theme.palette.set_hex(index, &value) {
|
||||
self.ui_theme.name = "Custom".to_string();
|
||||
self.active_theme = self.ui_theme.to_iced();
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ModelSpaceModeChanged(mode) => {
|
||||
self.model_space.mode = mode;
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ModelSpaceBgChanged(hex) => {
|
||||
self.model_bg_input = hex.clone();
|
||||
if hex.trim().is_empty() {
|
||||
self.model_space.custom_bg = None;
|
||||
self.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
} else if let Some(rgb) = crate::app::config::parse_hex(&hex) {
|
||||
self.model_space.custom_bg = Some(rgb);
|
||||
self.model_space.mode = crate::app::config::ModelSpaceMode::Custom;
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::PaperSpaceBgChanged(hex) => {
|
||||
self.paper_bg_input = hex.clone();
|
||||
if hex.trim().is_empty() {
|
||||
self.model_space.custom_paper_bg = None;
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
} else if let Some(rgb) = crate::app::config::parse_hex(&hex) {
|
||||
self.model_space.custom_paper_bg = Some(rgb);
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::DeskSpaceBgChanged(hex) => {
|
||||
self.desk_bg_input = hex.clone();
|
||||
if hex.trim().is_empty() {
|
||||
self.model_space.custom_desk_bg = None;
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
} else if let Some(rgb) = crate::app::config::parse_hex(&hex) {
|
||||
self.model_space.custom_desk_bg = Some(rgb);
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::GridOpacityChanged(opacity) => {
|
||||
self.model_space.grid_opacity = opacity.clamp(5, 100);
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SelectionAreaToggled(enabled) => {
|
||||
self.model_space.selection_area = enabled;
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SelectionOpacityChanged(opacity) => {
|
||||
self.model_space.selection_opacity = opacity.clamp(0, 100);
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SelectionWindowColorChanged(color) => {
|
||||
self.model_space.selection_window_color = color;
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SelectionCrossingColorChanged(color) => {
|
||||
self.model_space.selection_crossing_color = color;
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::SelectionHighlightColorChanged(color) => {
|
||||
self.model_space.selection_highlight_color = color;
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::GripSizeChanged(size) => {
|
||||
self.model_space.grip_size = size.clamp(1, 25);
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::GripColorChanged(color) => {
|
||||
self.model_space.grip_color = color;
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::GripHotChanged(color) => {
|
||||
self.model_space.grip_hot = color;
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::GripHoverChanged(color) => {
|
||||
self.model_space.grip_hover = color;
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::RestoreModelSpaceDisplayDefaults => {
|
||||
self.model_space.mode = crate::app::config::ModelSpaceMode::MatchTheme;
|
||||
self.model_space.custom_bg = None;
|
||||
self.model_space.custom_paper_bg = None;
|
||||
self.model_space.custom_desk_bg = None;
|
||||
self.model_space.grid_opacity = 18;
|
||||
self.model_bg_input.clear();
|
||||
self.paper_bg_input.clear();
|
||||
self.desk_bg_input.clear();
|
||||
self.sync_model_space_theme(true);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::RestoreSelectionVisualDefaults => {
|
||||
self.model_space.selection_area = true;
|
||||
self.model_space.selection_opacity = 12;
|
||||
self.model_space.selection_window_color = 0;
|
||||
self.model_space.selection_crossing_color = 0;
|
||||
self.model_space.selection_highlight_color = 0;
|
||||
self.model_space.selection_effect = true;
|
||||
self.model_space.selection_preview = 3;
|
||||
self.model_space.grip_size = 5;
|
||||
self.model_space.grip_color = 0;
|
||||
self.model_space.grip_hot = 0;
|
||||
self.model_space.grip_hover = 0;
|
||||
self.sync_model_space_theme(false);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::FileAssocChanged(enabled) => {
|
||||
// The same two calls FILEASSOC makes, so the checkbox and the
|
||||
// command cannot leave the setting and the registration
|
||||
@@ -5943,12 +6105,9 @@ impl OpenCADStudio {
|
||||
self.close_active_modal();
|
||||
return Task::none();
|
||||
};
|
||||
let model_bg = self.default_bg_color.unwrap_or([
|
||||
33.0 / 255.0,
|
||||
40.0 / 255.0,
|
||||
48.0 / 255.0,
|
||||
1.0,
|
||||
]);
|
||||
let model_bg = self.default_bg_color.unwrap_or_else(|| {
|
||||
self.model_space.resolve_model_bg(&self.active_theme)
|
||||
});
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(path) = opening.source_path.clone() {
|
||||
let current_fingerprint =
|
||||
|
||||
@@ -96,19 +96,3 @@ pub(super) 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).
|
||||
pub(super) 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.
|
||||
pub(super) 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]
|
||||
}
|
||||
@@ -2270,13 +2270,17 @@ impl OpenCADStudio {
|
||||
)
|
||||
})?
|
||||
});
|
||||
let highlights_hover = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.entity_pick_highlights_hover())
|
||||
.unwrap_or(false);
|
||||
let preview_in_command = (self.model_space.selection_preview & 2) != 0;
|
||||
let highlights_hover = preview_in_command
|
||||
&& self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.entity_pick_highlights_hover())
|
||||
.unwrap_or(false);
|
||||
if highlights_hover {
|
||||
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||
} else {
|
||||
self.tabs[i].scene.set_hover_highlight(None);
|
||||
}
|
||||
let hover_handle = hovered.unwrap_or(acadrust::Handle::NULL);
|
||||
let shift = self.shift_down;
|
||||
@@ -5118,7 +5122,12 @@ impl OpenCADStudio {
|
||||
);
|
||||
solid_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||
}
|
||||
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||
let preview_idle = (self.model_space.selection_preview & 1) != 0;
|
||||
if preview_idle {
|
||||
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||
} else {
|
||||
self.tabs[i].scene.set_hover_highlight(None);
|
||||
}
|
||||
self.hover_dwell = None;
|
||||
let hover_ms = hover_started.elapsed().as_secs_f64() * 1000.0;
|
||||
if perf && hover_ms >= 5.0 {
|
||||
|
||||
Reference in new issue
Block a user