From 38d81bf83805beceff947ed05673029161b22ab4 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Fri, 16 Oct 2026 01:07:23 +0300 Subject: [PATCH] feat(selection): highlight opacity and extra width (#1823) Options > Selection gains "Selection highlight opacity" and "Selection highlight extra width". Below full opacity the object's own colour shows through the highlight. The selection overlay draws with its own copy of the frame uniforms: transparency on, and every line widened by the extra width. Drawings opened at startup now get the selection colour settings too; they used to keep the defaults until a setting changed. Co-Authored-By: Claude Opus 5.5 --- locales/ar-SA/opencadstudio.ftl | 2 + locales/bg-BG/opencadstudio.ftl | 2 + locales/cs-CZ/opencadstudio.ftl | 2 + locales/de-DE/opencadstudio.ftl | 2 + locales/el-GR/opencadstudio.ftl | 2 + locales/en-US/opencadstudio.ftl | 2 + locales/es-ES/opencadstudio.ftl | 2 + locales/fi-FI/opencadstudio.ftl | 2 + locales/fr-FR/opencadstudio.ftl | 2 + locales/hi-IN/opencadstudio.ftl | 2 + locales/hu-HU/opencadstudio.ftl | 2 + locales/it-IT/opencadstudio.ftl | 2 + locales/ja-JP/opencadstudio.ftl | 2 + locales/ko-KR/opencadstudio.ftl | 2 + locales/lo-LA/opencadstudio.ftl | 2 + locales/nl-NL/opencadstudio.ftl | 2 + locales/pl-PL/opencadstudio.ftl | 2 + locales/pt-BR/opencadstudio.ftl | 2 + locales/ru-RU/opencadstudio.ftl | 2 + locales/tr-TR/opencadstudio.ftl | 2 + locales/zh-CN/opencadstudio.ftl | 2 + locales/zh-TW/opencadstudio.ftl | 2 + src/app/config.rs | 23 +++-- src/app/mod.rs | 2 + src/app/update/file.rs | 10 +- src/app/update/mod.rs | 14 +++ src/locale_catalog.rs | 4 + src/scene/mod.rs | 3 + src/scene/pipeline/mod.rs | 144 ++++++++++++++-------------- src/scene/pipeline/uniforms.rs | 6 +- src/scene/view/render.rs | 6 ++ src/shaders/block_wire.wgsl | 8 +- src/shaders/block_wire_storage.wgsl | 8 +- src/shaders/circle.wgsl | 8 +- src/shaders/ellipse.wgsl | 8 +- src/shaders/wire.wgsl | 8 +- src/shaders/wire_indexed.wgsl | 8 +- src/ui/window/options.rs | 32 +++++++ 38 files changed, 247 insertions(+), 89 deletions(-) diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 54486f961..e135b89d3 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -1625,6 +1625,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 80e2c2fb4..ce46a93f6 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -1612,6 +1612,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index f33b4c5b7..85a433500 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -1612,6 +1612,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 49488dc70..5bf936ee4 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -1613,6 +1613,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 2271ec48d..acf6e2b58 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -1614,6 +1614,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index fc6f231cd..236b34ea8 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -1613,6 +1613,8 @@ common = .window-selection-color = Window selection color .crossing-selection-color = Crossing selection color .selection-highlight-color = Selection highlight color + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = Grip Settings .grip-size = Grip size .unselected-grip-color = Unselected grip color diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 70d2c59a0..842c132a1 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -1613,6 +1613,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index e4a72c32f..419dfaa62 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -1612,6 +1612,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 02d85d828..738a361e4 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -1613,6 +1613,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 58492873c..ad0634ff0 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -1601,6 +1601,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 0a3492b63..2d788cda8 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -1618,6 +1618,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 228cd874d..73f7fc2a1 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -1612,6 +1612,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index be823af57..8c496cfa7 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -1603,6 +1603,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index ec8f98f83..5828cb7cd 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -1612,6 +1612,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/lo-LA/opencadstudio.ftl b/locales/lo-LA/opencadstudio.ftl index 786bbc3e1..9cbd0f602 100644 --- a/locales/lo-LA/opencadstudio.ftl +++ b/locales/lo-LA/opencadstudio.ftl @@ -1618,6 +1618,8 @@ common = .window-selection-color = ສີການເລືອກແບບ Window .crossing-selection-color = ສີການເລືອກແບບ Crossing .selection-highlight-color = ສີການເນັ້ນການເລືອກ + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = ການຕັ້ງຄ່າຈຸດກຣິບ .grip-size = ຂະໜາດຈຸດກຣິບ .unselected-grip-color = ສີຈຸດກຣິບທີ່ບໍ່ໄດ້ເລືອກ diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 308cba371..0e833a32c 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -1611,6 +1611,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index d0bc3dc97..a8c680f87 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -1612,6 +1612,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 6ca88dd6a..ee9061395 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -1613,6 +1613,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 10a5f642d..f432c23e7 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -1616,6 +1616,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 189759229..d9fd70437 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -1599,6 +1599,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 9751345d2..5c90421f8 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -1600,6 +1600,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 8febd85e8..2e7b7164b 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -1612,6 +1612,8 @@ common = .window-selection-color = { common.selection }: { common.window }: { common.color } .crossing-selection-color = { common.selection }: { common.crossing }: { common.color } .selection-highlight-color = { common.selection }: { common.highlight }: { common.color } + .selection-highlight-opacity = Selection highlight opacity + .selection-highlight-extra-width = Selection highlight extra width .grip-settings = { common.grips }: { properties.settings } .grip-size = { common.grips }: { common.size } .unselected-grip-color = { common.grips }: { common.unselected }: { common.color } diff --git a/src/app/config.rs b/src/app/config.rs index 408fa6679..3f8b8f534 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -357,6 +357,11 @@ pub struct ModelSpaceThemeConfig { pub selection_crossing_color: u8, /// SELECTIONEFFECTCOLOR: ACI index for selected entities highlight (0 = Theme Primary, 1..=255 = ACI). pub selection_highlight_color: u8, + /// Selection highlight opacity in percent (10–100); below 100 the + /// object's own colour shows through. + pub selection_highlight_opacity: u8, + /// Extra screen pixels the selection highlight is drawn wider (0–6). + pub selection_highlight_widen: u8, /// SELECTIONEFFECT: whether selected objects glow with solid highlight (true) or dash (false). pub selection_effect: bool, /// SELECTIONPREVIEW bitmask: 1 = idle, 2 = during a command, 3 = both. @@ -386,6 +391,8 @@ impl Default for ModelSpaceThemeConfig { selection_window_color: 0, selection_crossing_color: 0, selection_highlight_color: 0, + selection_highlight_opacity: 100, + selection_highlight_widen: 0, selection_effect: true, selection_preview: 3, grip_size: 5, @@ -437,15 +444,13 @@ impl ModelSpaceThemeConfig { /// Resolve active selection highlight overlay color as [f32; 4]. pub fn resolve_selection_color(&self) -> [f32; 4] { - if self.selection_highlight_color == 0 { - crate::scene::model::wire_model::WireModel::SELECTED - } else if let Some((r, g, b)) = - codec::types::aci_table::aci_to_rgb(self.selection_highlight_color) - { - [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0] - } else { - crate::scene::model::wire_model::WireModel::SELECTED - } + let mut color = codec::types::aci_table::aci_to_rgb(self.selection_highlight_color) + .filter(|_| self.selection_highlight_color != 0) + .map_or(crate::scene::model::wire_model::WireModel::SELECTED, |(r, g, b)| { + [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0] + }); + color[3] = f32::from(self.selection_highlight_opacity.clamp(10, 100)) / 100.0; + color } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 163dabb90..f81649499 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2674,6 +2674,8 @@ pub enum Message { SelectionAreaToggled(bool), /// Change Selection Area opacity percentage (0–100, SELECTIONAREAOPACITY). SelectionOpacityChanged(u8), + SelectionHighlightOpacityChanged(u8), + SelectionHighlightWidenChanged(u8), /// Change Window selection color ACI index (0 = Theme Primary, 1..=255 = ACI). SelectionWindowColorChanged(u8), /// Change Crossing selection color ACI index (0 = Theme Success, 1..=255 = ACI). diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 5a8ba9737..fecfbb10f 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -1278,6 +1278,9 @@ impl OpenCADStudio { return; } tab.scene.model_lineweight_scale = self.lineweight_display_scale as f32 / 100.0; + tab.scene.selection_color = self.model_space.resolve_selection_color(); + tab.scene.selection_effect = self.model_space.selection_effect; + tab.scene.selection_widen = f32::from(self.model_space.selection_highlight_widen.min(6)); if bg.is_none() && paper_bg.is_none() { return; } @@ -1551,6 +1554,7 @@ impl OpenCADStudio { 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; + let sel_widen = f32::from(self.model_space.selection_highlight_widen.min(6)); self.default_bg_color = Some(model_bg); self.default_paper_bg_color = Some(paper_bg); @@ -1563,10 +1567,14 @@ impl OpenCADStudio { tab.scene.paper_bg_color = paper_bg; tab.scene.selection_color = sel_color; tab.scene.selection_effect = sel_effect; + let old_sel_widen = std::mem::replace(&mut tab.scene.selection_widen, sel_widen); tab.bg_color = Some(model_bg); tab.paper_bg_color = Some(paper_bg); - if old_sel_color != sel_color || old_sel_effect != sel_effect { + if old_sel_color != sel_color + || old_sel_effect != sel_effect + || old_sel_widen != sel_widen + { tab.scene.selection_generation = tab.scene.selection_generation.wrapping_add(1); } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index ed713554c..b0e36bbe5 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -9175,6 +9175,20 @@ impl OpenCADStudio { Task::none() } + Message::SelectionHighlightOpacityChanged(opacity) => { + self.model_space.selection_highlight_opacity = opacity.clamp(10, 100); + self.sync_model_space_theme(false); + self.persist_settings_if_changed(); + Task::none() + } + + Message::SelectionHighlightWidenChanged(widen) => { + self.model_space.selection_highlight_widen = widen.min(6); + 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); diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index e367c7b5c..33b411619 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -5183,6 +5183,10 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Window selection color" => Some(("common", "window-selection-color")), "Crossing selection color" => Some(("common", "crossing-selection-color")), "Selection highlight color" => Some(("common", "selection-highlight-color")), + "Selection highlight opacity" => Some(("common", "selection-highlight-opacity")), + "Selection highlight extra width" => { + Some(("common", "selection-highlight-extra-width")) + } "Grip Settings" => Some(("common", "grip-settings")), "Grip size" => Some(("common", "grip-size")), "Unselected grip color" => Some(("common", "unselected-grip-color")), diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 5a070a2d8..d90117db5 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -2204,6 +2204,8 @@ pub struct Scene { pub selection_color: [f32; 4], /// Whether selection visual effect (glow/highlight) is enabled (SELECTIONEFFECT). pub selection_effect: bool, + /// Extra screen pixels the selection highlight is drawn wider. + pub selection_widen: f32, /// Whether entity transparency is honoured on screen. When false the /// wire shader forces every line opaque (a uniform toggle, no retessellate). pub transparency_display: bool, @@ -2827,6 +2829,7 @@ impl Scene { constraint_hover_refs: Vec::new(), constraint_hover_wires: Vec::new(), selection_color: crate::scene::model::wire_model::WireModel::SELECTED, + selection_widen: 0.0, selection_effect: true, transparency_display: true, model_lineweight_scale: 1.0, diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index f669d3267..466877e15 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -182,6 +182,10 @@ pub struct Pipeline { uniform_buffer: wgpu::Buffer, frame_bgl: wgpu::BindGroupLayout, uniform_bind_group: wgpu::BindGroup, + /// The selection overlay's copy of the frame uniforms: transparency on, + /// so a translucent highlight colour shows, and lines widened. + selection_uniform_buffer: wgpu::Buffer, + selection_uniform_bind_group: wgpu::BindGroup, shadow_uniform_bind_group: wgpu::BindGroup, background_texture: wgpu::Texture, environment_texture: wgpu::Texture, @@ -414,6 +418,37 @@ pub struct Pipeline { pub slot_id: u64, } +/// The views and samplers the frame bind group carries beside its uniforms. +struct FrameViews<'a> { + background: &'a wgpu::TextureView, + environment: &'a wgpu::TextureView, + background_sampler: &'a wgpu::Sampler, + shadow: &'a wgpu::TextureView, + shadow_sampler: &'a wgpu::Sampler, +} + +fn frame_bind_group( + device: &wgpu::Device, + layout: &wgpu::BindGroupLayout, + uniforms: &wgpu::Buffer, + views: &FrameViews<'_>, +) -> wgpu::BindGroup { + let entry = |binding, resource| wgpu::BindGroupEntry { binding, resource }; + device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("viewer.bind_group"), + layout, + entries: &[ + entry(0, uniforms.as_entire_binding()), + entry(1, wgpu::BindingResource::TextureView(views.background)), + entry(2, wgpu::BindingResource::Sampler(views.background_sampler)), + entry(3, wgpu::BindingResource::TextureView(views.environment)), + entry(4, wgpu::BindingResource::Sampler(views.background_sampler)), + entry(5, wgpu::BindingResource::TextureView(views.shadow)), + entry(6, wgpu::BindingResource::Sampler(views.shadow_sampler)), + ], + }) +} + impl Pipeline { pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self { // ── Shared frame uniform buffer (view_proj etc.) ─────────────────── @@ -423,6 +458,12 @@ impl Pipeline { usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, mapped_at_creation: false, }); + let selection_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("viewer.selection_uniform_buffer"), + size: std::mem::size_of::() as u64, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); // Bind group layout 0 — shared by wire and hatch pipelines. let frame_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -563,40 +604,17 @@ impl Pipeline { background_texture.create_view(&wgpu::TextureViewDescriptor::default()); let environment_view = environment_texture.create_view(&wgpu::TextureViewDescriptor::default()); - let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("viewer.bind_group"), - layout: &frame_bgl, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: uniform_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&background_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&background_sampler), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::TextureView(&environment_view), - }, - wgpu::BindGroupEntry { - binding: 4, - resource: wgpu::BindingResource::Sampler(&background_sampler), - }, - wgpu::BindGroupEntry { - binding: 5, - resource: wgpu::BindingResource::TextureView(&shadow_fallback_view), - }, - wgpu::BindGroupEntry { - binding: 6, - resource: wgpu::BindingResource::Sampler(&shadow_sampler), - }, - ], - }); + let frame_views = FrameViews { + background: &background_view, + environment: &environment_view, + background_sampler: &background_sampler, + shadow: &shadow_fallback_view, + shadow_sampler: &shadow_sampler, + }; + let uniform_bind_group = + frame_bind_group(device, &frame_bgl, &uniform_buffer, &frame_views); + let selection_uniform_bind_group = + frame_bind_group(device, &frame_bgl, &selection_uniform_buffer, &frame_views); let shadow_uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("shadow.frame_bind_group"), layout: &shadow_frame_bgl, @@ -2390,6 +2408,8 @@ impl Pipeline { uniform_buffer, frame_bgl, uniform_bind_group, + selection_uniform_buffer, + selection_uniform_bind_group, shadow_uniform_bind_group, background_texture, environment_texture, @@ -3926,40 +3946,18 @@ analytic={:.1} regular={:.1} blocks={:.1}", let environment_view = self .environment_texture .create_view(&wgpu::TextureViewDescriptor::default()); - self.uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - label: Some("viewer.bind_group"), - layout: &self.frame_bgl, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: self.uniform_buffer.as_entire_binding(), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::TextureView(&background_view), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::Sampler(&self.background_sampler), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::TextureView(&environment_view), - }, - wgpu::BindGroupEntry { - binding: 4, - resource: wgpu::BindingResource::Sampler(&self.background_sampler), - }, - wgpu::BindGroupEntry { - binding: 5, - resource: wgpu::BindingResource::TextureView(self.shadow_view()), - }, - wgpu::BindGroupEntry { - binding: 6, - resource: wgpu::BindingResource::Sampler(&self.shadow_sampler), - }, - ], - }); + let views = FrameViews { + background: &background_view, + environment: &environment_view, + background_sampler: &self.background_sampler, + shadow: self.shadow_view(), + shadow_sampler: &self.shadow_sampler, + }; + let frame = frame_bind_group(device, &self.frame_bgl, &self.uniform_buffer, &views); + let selection = + frame_bind_group(device, &self.frame_bgl, &self.selection_uniform_buffer, &views); + self.uniform_bind_group = frame; + self.selection_uniform_bind_group = selection; } /// Allocate or drop the shadow target to match `shadow_enabled`. @@ -3995,7 +3993,11 @@ analytic={:.1} regular={:.1} blocks={:.1}", if self.shadow_full.is_none() { uniforms.shadow_params[0] = 0.0; } + let mut selection = uniforms; + selection.transparency_enable = 1.0; + uniforms.highlight_widen = 0.0; queue.write_buffer(&self.uniform_buffer, 0, bytemuck::bytes_of(&uniforms)); + queue.write_buffer(&self.selection_uniform_buffer, 0, bytemuck::bytes_of(&selection)); } pub fn upload_background_images( @@ -5120,7 +5122,7 @@ analytic={:.1} regular={:.1} blocks={:.1}", multiview_mask: None, }); pass.set_viewport(raster.x, raster.y, raster.width, raster.height, 0.0, 1.0); - pass.set_bind_group(0, &self.uniform_bind_group, &[]); + pass.set_bind_group(0, &self.selection_uniform_bind_group, &[]); pass.set_stencil_reference(stencil_ref); if !self.gpu_selected_wires.is_empty() { pass.set_pipeline(if occluded { @@ -5160,7 +5162,7 @@ analytic={:.1} regular={:.1} blocks={:.1}", } else { &self.circle_xray_pipeline }); - pass.set_bind_group(0, &self.uniform_bind_group, &[]); + pass.set_bind_group(0, &self.selection_uniform_bind_group, &[]); for cg in &self.gpu_selected_circles { if cg.instance_count > 0 { pass.set_vertex_buffer(0, cg.instance_buffer.slice(..)); @@ -5174,7 +5176,7 @@ analytic={:.1} regular={:.1} blocks={:.1}", } else { &self.ellipse_xray_pipeline }); - pass.set_bind_group(0, &self.uniform_bind_group, &[]); + pass.set_bind_group(0, &self.selection_uniform_bind_group, &[]); for eg in &self.gpu_selected_ellipses { if eg.instance_count > 0 { pass.set_vertex_buffer(0, eg.instance_buffer.slice(..)); diff --git a/src/scene/pipeline/uniforms.rs b/src/scene/pipeline/uniforms.rs index 06cb0cc1e..fe84756e9 100644 --- a/src/scene/pipeline/uniforms.rs +++ b/src/scene/pipeline/uniforms.rs @@ -39,7 +39,9 @@ pub struct Uniforms { // jitter. pub view_rot: glam::Mat4, pub eye_high: [f32; 3], - pub _pad_eh: f32, + /// Extra screen pixels added to every line width. Only the selection + /// overlay pass sees a non-zero value (`Pipeline::upload_uniforms`). + pub highlight_widen: f32, pub eye_low: [f32; 3], pub _pad_el: f32, @@ -121,7 +123,7 @@ impl Uniforms { lineweight_scale: 1.0, view_rot: camera.view_proj_rte(bounds), eye_high, - _pad_eh: 0.0, + highlight_widen: 0.0, eye_low, _pad_el: 0.0, light_position_type: [[0.0; 4]; 4], diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index c42d93184..bf035050f 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -270,6 +270,8 @@ pub struct Primitive { pub(in crate::scene) selection_color: [f32; 4], /// Whether selection visual effect (glow/highlight) is enabled. pub(in crate::scene) selection_effect: bool, + /// Extra screen pixels the selection highlight is drawn wider. + pub(in crate::scene) selection_widen: f32, /// One input-to-render sample, carried only when PERF tracing is enabled. pub(in crate::scene) nav_perf: Option, } @@ -4298,6 +4300,7 @@ impl Scene { viewcube_text_color, selection_color: self.selection_color, selection_effect: self.selection_effect, + selection_widen: self.selection_widen, nav_perf: perf_nav, } } @@ -4330,6 +4333,7 @@ impl Scene { viewcube_text_color, selection_color: self.selection_color, selection_effect: self.selection_effect, + selection_widen: self.selection_widen, nav_perf: None, }; }; @@ -4392,6 +4396,7 @@ impl Scene { viewcube_text_color, selection_color: self.selection_color, selection_effect: self.selection_effect, + selection_widen: self.selection_widen, nav_perf: perf_nav, } } @@ -4688,6 +4693,7 @@ impl Scene { uniforms.viewport_size = [visible_w, visible_h]; uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 }; uniforms.transparency_enable = if self.transparency_display { 1.0 } else { 0.0 }; + uniforms.highlight_widen = self.selection_widen; uniforms.viewport_background = display.background.base; uniforms.view_tone = [display.brightness, display.contrast, 0.0, 0.0]; uniforms.background_top = display.background.colors[0]; diff --git a/src/shaders/block_wire.wgsl b/src/shaders/block_wire.wgsl index 8399a2c2f..cca43637f 100644 --- a/src/shaders/block_wire.wgsl +++ b/src/shaders/block_wire.wgsl @@ -8,7 +8,7 @@ struct Uniforms { lineweight_scale: f32, view_rot: mat4x4, eye_high: vec3, - _pad_eh: f32, + highlight_widen: f32, eye_low: vec3, _pad_el: f32, } @@ -63,7 +63,13 @@ struct VertexOut { @location(9) @interpolate(flat) cap_ends: vec3, } +// The selection pass widens highlighted lines by `highlight_widen` pixels; +// every other pass uploads zero. fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { + return resolve_hw_base(taper_ratio, world_hw, px_hw) + u.highlight_widen * 0.5; +} + +fn resolve_hw_base(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { if taper_ratio > 0.0 { return max((taper_ratio * world_hw) / u.world_per_pixel, 0.5); } diff --git a/src/shaders/block_wire_storage.wgsl b/src/shaders/block_wire_storage.wgsl index ebb5a7ea6..e79268b23 100644 --- a/src/shaders/block_wire_storage.wgsl +++ b/src/shaders/block_wire_storage.wgsl @@ -27,7 +27,7 @@ struct Uniforms { lineweight_scale: f32, view_rot: mat4x4, eye_high: vec3, - _pad_eh: f32, + highlight_widen: f32, eye_low: vec3, _pad_el: f32, } @@ -87,7 +87,13 @@ struct VertexOut { @location(9) @interpolate(flat) cap_ends: vec3, } +// The selection pass widens highlighted lines by `highlight_widen` pixels; +// every other pass uploads zero. fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { + return resolve_hw_base(taper_ratio, world_hw, px_hw) + u.highlight_widen * 0.5; +} + +fn resolve_hw_base(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { if taper_ratio > 0.0 { return max((taper_ratio * world_hw) / u.world_per_pixel, 0.5); } diff --git a/src/shaders/circle.wgsl b/src/shaders/circle.wgsl index ef6bbf99d..c923c112f 100644 --- a/src/shaders/circle.wgsl +++ b/src/shaders/circle.wgsl @@ -20,7 +20,7 @@ struct Uniforms { // Relative-to-eye (double-single) view_rot: mat4x4, eye_high: vec3, - _pad_eh: f32, + highlight_widen: f32, eye_low: vec3, _pad_el: f32, } @@ -56,7 +56,13 @@ struct VertexOut { @location(10) @interpolate(flat) is_world: f32, } +// The selection pass widens highlighted lines by `highlight_widen` pixels; +// every other pass uploads zero. fn resolve_px_hw(px_hw: f32) -> f32 { + return resolve_px_hw_base(px_hw) + u.highlight_widen * 0.5; +} + +fn resolve_px_hw_base(px_hw: f32) -> f32 { var display_hw = max(px_hw * u.lineweight_scale, 0.5); if u.lineweight_scale < 0.0 { let scale = -u.lineweight_scale; diff --git a/src/shaders/ellipse.wgsl b/src/shaders/ellipse.wgsl index 2fbe20ab5..ee6bc13e4 100644 --- a/src/shaders/ellipse.wgsl +++ b/src/shaders/ellipse.wgsl @@ -15,7 +15,7 @@ struct Uniforms { // Relative-to-eye (double-single) view_rot: mat4x4, eye_high: vec3, - _pad_eh: f32, + highlight_widen: f32, eye_low: vec3, _pad_el: f32, } @@ -50,7 +50,13 @@ struct VertexOut { @location(9) @interpolate(flat) end_param: f32, } +// The selection pass widens highlighted lines by `highlight_widen` pixels; +// every other pass uploads zero. fn resolve_hw(px_hw: f32) -> f32 { + return resolve_hw_base(px_hw) + u.highlight_widen * 0.5; +} + +fn resolve_hw_base(px_hw: f32) -> f32 { var display_hw = max(px_hw * u.lineweight_scale, 0.5); if u.lineweight_scale < 0.0 { let scale = -u.lineweight_scale; diff --git a/src/shaders/wire.wgsl b/src/shaders/wire.wgsl index 4d312896f..40c0d5730 100644 --- a/src/shaders/wire.wgsl +++ b/src/shaders/wire.wgsl @@ -37,7 +37,7 @@ struct Uniforms { // jitter on pan / zoom / rotate. view_rot: mat4x4, eye_high: vec3, - _pad_eh: f32, + highlight_widen: f32, eye_low: vec3, _pad_el: f32, } @@ -98,7 +98,13 @@ struct VertexOut { // Half-width of one segment end: a tapered band's own end width wins, then a // constant world-unit band, then the screen-pixel lineweight (LWDISPLAY off // collapses to a hairline). +// The selection pass widens highlighted lines by `highlight_widen` pixels; +// every other pass uploads zero. fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 { + return resolve_hw_base(taper, world_hw, px_hw) + u.highlight_widen * 0.5; +} + +fn resolve_hw_base(taper: f32, world_hw: f32, px_hw: f32) -> f32 { if taper >= 0.0 { return max(taper / u.world_per_pixel, 0.5); } if world_hw > 0.0 { return max(world_hw / u.world_per_pixel, 0.5); } if world_hw < 0.0 { return max(-world_hw, 0.5); } diff --git a/src/shaders/wire_indexed.wgsl b/src/shaders/wire_indexed.wgsl index 162fdbcd2..e7b7722fe 100644 --- a/src/shaders/wire_indexed.wgsl +++ b/src/shaders/wire_indexed.wgsl @@ -15,7 +15,7 @@ struct Uniforms { lineweight_scale: f32, view_rot: mat4x4, eye_high: vec3, - _pad_eh: f32, + highlight_widen: f32, eye_low: vec3, _pad_el: f32, } @@ -79,7 +79,13 @@ struct VertexOut { // Half-width of one segment end: a tapered band's own end width wins, then a // constant world-unit band, then the screen-pixel lineweight (LWDISPLAY off // collapses to a hairline). +// The selection pass widens highlighted lines by `highlight_widen` pixels; +// every other pass uploads zero. fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { + return resolve_hw_base(taper_ratio, world_hw, px_hw) + u.highlight_widen * 0.5; +} + +fn resolve_hw_base(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 { if taper_ratio > 0.0 { return max((taper_ratio * world_hw) / u.world_per_pixel, 0.5); } diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index 87318baba..a28174dea 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -1120,6 +1120,38 @@ pub fn view_window<'a>( ] .spacing(10) .align_y(iced::Center), + Space::new().height(10), + row![ + text(crate::t!("Selection highlight opacity")).size(12).width(140), + slider( + 10..=100, + model_space.selection_highlight_opacity.clamp(10, 100), + Message::SelectionHighlightOpacityChanged, + ) + .step(5) + .width(Fill), + text(format!("{}%", model_space.selection_highlight_opacity.clamp(10, 100))) + .size(11) + .width(44), + ] + .spacing(10) + .align_y(iced::Center), + Space::new().height(10), + row![ + text(crate::t!("Selection highlight extra width")).size(12).width(140), + slider( + 0..=6, + model_space.selection_highlight_widen.min(6), + Message::SelectionHighlightWidenChanged, + ) + .step(1) + .width(Fill), + text(format!("{} px", model_space.selection_highlight_widen.min(6))) + .size(11) + .width(44), + ] + .spacing(10) + .align_y(iced::Center), Space::new().height(24), text(crate::t!("Preview")).size(15), Space::new().height(10),