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

commit5bb07c8d6ab722fa70ae141f99edcdd13322ea77
parent8a2ba4ad1b
authorLucas Galante <[email protected]>
date2026-07-31 13:18
feat: wireframe draws atop the fill, with a single-color mode

Show Wireframe no longer replaces the filled geometry — the wire pass
always rides on top of it. Two new Render-node params control the wire
look: 'Wire Single Color' toggles between the geometry's vertex colors
(darkened 45% so the wires separate from the identical fill) and a flat
color; 'Wire Color' picks that color. Both respect the geometry
Opacity.

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

 src/app.rs     | 37 ++++++++++++++++++++++++++++++++-----
 src/project.rs |  8 ++++++++
 2 files changed, 40 insertions(+), 5 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index ec9f839..3dcace1 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -985,12 +985,17 @@ pub struct State {
     pub last_viewport_active_camera: String,
     pub last_viewport_show_viewport: bool,
     /// Wireframe display of the node geometry (the Render utility node's
-    /// "Show Wireframe" toggle).
+    /// "Show Wireframe" toggle): a wire pass drawn IN ADDITION to the filled
+    /// geometry, never instead of it.
     pub wireframe: bool,
     pub last_viewport_wireframe: bool,
-    /// Draw the wireframe OVER the shaded geometry (the Render node's
-    /// "Wireframe Overlay" toggle) — edges visualized without giving up the
-    /// filled primitives. Wins over `wireframe` when both are set.
+    /// "Wire Single Color" toggle: on, the wires draw in `wire_color`; off,
+    /// they keep the geometry's vertex colors (darkened enough to separate
+    /// from the identical fill beneath).
+    pub wire_single_color: bool,
+    pub wire_color: [f32; 3],
+    pub last_viewport_wire_single_color: bool,
+    pub last_viewport_wire_color: [f32; 3],
     /// Opacity of the rendered node geometry (the Render node's "Opacity"
     /// slider): 1.0 opaque, straight-alpha blended toward the viewport bg.
     pub geo_opacity: f32,
@@ -2822,6 +2827,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             last_viewport_show_viewport: false,
             wireframe: false,
             last_viewport_wireframe: false,
+            wire_single_color: false,
+            wire_color: [1.0, 1.0, 1.0],
+            last_viewport_wire_single_color: false,
+            last_viewport_wire_color: [1.0, 1.0, 1.0],
             geo_opacity: 1.0,
             last_viewport_geo_opacity: 1.0,
             render_points: false,
@@ -5355,6 +5364,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     || self.last_viewport_show_viewport != self.show_viewport
                     || self.last_viewport_wireframe != self.wireframe
                     || self.last_viewport_geo_opacity != self.geo_opacity
+                    || self.last_viewport_wire_single_color != self.wire_single_color
+                    || self.last_viewport_wire_color != self.wire_color
                     || self.last_viewport_render_points != self.render_points
                     || self.last_viewport_point_size != self.point_size
                     || self.last_viewport_point_color != self.point_color;
@@ -5396,7 +5407,21 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                         draws.push(SceneDraw { mesh: meshes.points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity });
                     }
                     if self.vertex_count_spheres > 0 {
-                        draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: self.wireframe, wire_tint: NO_TINT, opacity: geo_opacity });
+                        draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity });
+                        if self.wireframe {
+                            // The wire pass rides ON TOP of the fill (never
+                            // replaces it). Single-color mode replaces the
+                            // fragment color outright; geometry-color mode
+                            // keeps the vertex colors, darkened 45% so the
+                            // wires separate from the identical fill beneath
+                            // (a full-strength copy would vanish into it).
+                            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.45]
+                            };
+                            draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: true, wire_tint: tint, opacity: geo_opacity });
+                        }
                     }
                     renderer.stage_scene((sx, sy, cw, ch), draws);
                     }
@@ -5421,6 +5446,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     self.last_viewport_show_viewport = self.show_viewport;
                     self.last_viewport_wireframe = self.wireframe;
                     self.last_viewport_geo_opacity = self.geo_opacity;
+                    self.last_viewport_wire_single_color = self.wire_single_color;
+                    self.last_viewport_wire_color = self.wire_color;
                     self.last_viewport_render_points = self.render_points;
                     self.last_viewport_point_size = self.point_size;
                     self.last_viewport_point_color = self.point_color;
diff --git a/src/project.rs b/src/project.rs
index b2326a1..5d08a68 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -270,6 +270,8 @@ impl State {
         let show_spreadsheet = self.show_spreadsheet;
         let show_playbar = self.show_playbar;
         let wireframe = self.wireframe;
+        let wire_single_color = self.wire_single_color;
+        let wire_color = self.wire_color;
         let geo_opacity = self.geo_opacity;
         let render_points = self.render_points;
         let point_size = self.point_size;
@@ -526,6 +528,8 @@ 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);
         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);
         ensure_param(render_node, "Point Size", "slider:0.01:0.30", &format!("{:.2}", point_size), &[], Some(0.01), Some(0.30), None);
@@ -534,6 +538,8 @@ impl State {
         for p in render_node.params.iter_mut() {
             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),
                 "Opacity" => p.default = format!("{:.2}", geo_opacity),
                 "Render Points" => set_toggle(p, render_points),
                 "Point Size" => p.default = format!("{:.2}", point_size),
@@ -673,6 +679,8 @@ impl State {
             for p in &params {
                 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; }
                     "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; }
                     "Point Size" => if let Ok(val) = p.default.parse::<f32>() { self.point_size = val.clamp(0.005, 1.0); }