git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit7217ffc46b0973b2b2f4736185e806038c7324d1
parent224a0514ef
authorLucas Galante <[email protected]>
date2026-07-31 14:16
feat: Wire Color is rgba — its alpha is the wireframe's opacity; geometry Opacity is polygons-only

The wire pass no longer inherits the Opacity slider: wires fade via the
Wire Color alpha channel instead, in both color modes. Older saves'
plain-rgb Wire Color migrates to rgba on load.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/app.rs     | 16 +++++++++++-----
 src/project.rs | 26 +++++++++++++++++++++++---
 2 files changed, 34 insertions(+), 8 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 2e3deb0..c6a77a1 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -997,11 +997,13 @@ pub struct State {
     /// they carry the geometry's vertex colors unlit — brighter than the lit
     /// fill beneath, which is what separates them.
     pub wire_single_color: bool,
-    pub wire_color: [f32; 3],
+    /// RGBA: the alpha channel is the wireframe's OWN opacity in both color
+    /// modes — the geometry Opacity slider affects only the polygons.
+    pub wire_color: [f32; 4],
     /// Wire line width in framebuffer pixels ("Wire Thickness" slider).
     pub wire_width: f32,
     pub last_viewport_wire_single_color: bool,
-    pub last_viewport_wire_color: [f32; 3],
+    pub last_viewport_wire_color: [f32; 4],
     pub last_viewport_wire_width: f32,
     /// Opacity of the rendered node geometry (the Render node's "Opacity"
     /// slider): 1.0 opaque, straight-alpha blended toward the viewport bg.
@@ -2835,10 +2837,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             wireframe: false,
             last_viewport_wireframe: false,
             wire_single_color: false,
-            wire_color: [1.0, 1.0, 1.0],
+            wire_color: [1.0, 1.0, 1.0, 1.0],
             wire_width: 1.0,
             last_viewport_wire_single_color: false,
-            last_viewport_wire_color: [1.0, 1.0, 1.0],
+            last_viewport_wire_color: [1.0, 1.0, 1.0, 1.0],
             last_viewport_wire_width: 1.0,
             geo_opacity: 1.0,
             last_viewport_geo_opacity: 1.0,
@@ -5441,12 +5443,16 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                             // fill beneath. Far-side wires that clear the
                             // depth test near the limb show as their own
                             // (complementary) colors — a soft x-ray read.
+                            // The wires' opacity is the Wire Color ALPHA in
+                            // both modes; the geometry Opacity slider is
+                            // polygons-only.
                             let tint = if self.wire_single_color {
                                 [self.wire_color[0], self.wire_color[1], self.wire_color[2], 1.0]
                             } else {
                                 [0.0, 0.0, 0.0, 0.0]
                             };
-                            draws.push(SceneDraw { mesh: meshes.sphere_edges, mvp, wireframe: true, wire_tint: tint, opacity: geo_opacity, line_width: self.wire_width, wire_base_width: 0.0 });
+                            let wire_alpha = self.wire_color[3].clamp(0.0, 1.0);
+                            draws.push(SceneDraw { mesh: meshes.sphere_edges, mvp, wireframe: true, wire_tint: tint, opacity: wire_alpha, line_width: self.wire_width, wire_base_width: 0.0 });
                         }
                     }
                     renderer.stage_scene((sx, sy, cw, ch), draws);
diff --git a/src/project.rs b/src/project.rs
index aed6924..7bc4b57 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -15,6 +15,20 @@ fn hex_to_color(hex: &str) -> Option<[f32; 3]> {
     cce_ui::color::parse_hex_rgb(hex)
 }
 
+fn color_to_hex8(rgba: [f32; 4]) -> String {
+    format!("#{:02x}{:02x}{:02x}{:02x}",
+        (rgba[0] * 255.0).round().clamp(0.0, 255.0) as u8,
+        (rgba[1] * 255.0).round().clamp(0.0, 255.0) as u8,
+        (rgba[2] * 255.0).round().clamp(0.0, 255.0) as u8,
+        (rgba[3] * 255.0).round().clamp(0.0, 255.0) as u8
+    )
+}
+
+/// 6- or 8-digit hex → RGBA (alpha 1.0 when absent).
+fn hex_to_rgba(hex: &str) -> Option<[f32; 4]> {
+    cce_ui::color::parse_hex_rgba(hex)
+}
+
 impl State {
 
 
@@ -530,7 +544,9 @@ impl State {
         ensure_param(render_node, "Render Settings", "section", "", &[], None, None, None);
         ensure_param(render_node, "Show Wireframe", "toggle", bool_str(wireframe), &[], None, None, None);
         ensure_param(render_node, "Wire Single Color", "toggle", bool_str(wire_single_color), &[], None, None, None);
-        ensure_param(render_node, "Wire Color", "color", &color_to_hex(wire_color), &[], None, None, None);
+        // rgba: the alpha channel is the wireframe's own opacity (the
+        // geometry Opacity slider deliberately leaves wires alone).
+        ensure_param(render_node, "Wire Color", "rgba", &color_to_hex8(wire_color), &[], None, None, None);
         ensure_param(render_node, "Wire Thickness", "slider:1.0:8.0:1", &format!("{:.1}", wire_width), &[], Some(1.0), Some(8.0), None);
         ensure_param(render_node, "Opacity", "slider:0.00:1.00", &format!("{:.2}", geo_opacity), &[], Some(0.0), Some(1.0), None);
         ensure_param(render_node, "Render Points", "toggle", bool_str(render_points), &[], None, None, None);
@@ -541,7 +557,11 @@ impl State {
             match p.name.as_str() {
                 "Show Wireframe" => set_toggle(p, wireframe),
                 "Wire Single Color" => set_toggle(p, wire_single_color),
-                "Wire Color" => p.default = color_to_hex(wire_color),
+                "Wire Color" => {
+                    // Type migration: early saves carried a plain rgb color.
+                    p.param_type = "rgba".to_string();
+                    p.default = color_to_hex8(wire_color);
+                }
                 "Wire Thickness" => p.default = format!("{:.1}", wire_width),
                 "Opacity" => p.default = format!("{:.2}", geo_opacity),
                 "Render Points" => set_toggle(p, render_points),
@@ -690,7 +710,7 @@ impl State {
                 match p.name.as_str() {
                     "Show Wireframe" => if let Ok(val) = p.default.parse::<bool>() { self.wireframe = val; }
                     "Wire Single Color" => if let Ok(val) = p.default.parse::<bool>() { self.wire_single_color = val; }
-                    "Wire Color" => if let Some(col) = hex_to_color(&p.default) { self.wire_color = col; }
+                    "Wire Color" => if let Some(col) = hex_to_rgba(&p.default) { self.wire_color = col; }
                     "Wire Thickness" => if let Ok(val) = p.default.parse::<f32>() { self.wire_width = val.clamp(1.0, 8.0); }
                     "Opacity" => if let Ok(val) = p.default.parse::<f32>() { self.geo_opacity = val.clamp(0.0, 1.0); }
                     "Render Points" => if let Ok(val) = p.default.parse::<bool>() { self.render_points = val; }