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

commit8a2ba4ad1bd45225176fffe49f991e9cd65710f4
parentb31758b45e
authorLucas Galante <[email protected]>
date2026-07-31 13:09
feat: Render-node Opacity slider; retire the Wireframe Overlay toggle

Opacity (0-1, default 1) blends the node geometry — the spheres mesh
and rendered points — over the viewport; scene furniture stays opaque.
The Wireframe Overlay toggle is gone: param dropped from the Render
node (purged from older saves on load), overlay draw branch and state
removed. Show Wireframe now always renders the full cage.

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

 src/app.rs      | 48 +++++++++++++++++++-----------------------------
 src/project.rs  | 12 ++++++++----
 src/vk_smoke.rs |  6 +++---
 3 files changed, 30 insertions(+), 36 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 9d38793..ec9f839 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -991,8 +991,10 @@ pub struct State {
     /// 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.
-    pub wireframe_overlay: bool,
-    pub last_viewport_wireframe_overlay: bool,
+    /// 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,
+    pub last_viewport_geo_opacity: f32,
     /// Point display of the node geometry (the Render node's "Render Points"
     /// toggle): one small octahedron per distinct vertex, sized by
     /// "Point Size" and tinted by "Point Color".
@@ -2820,8 +2822,8 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             last_viewport_show_viewport: false,
             wireframe: false,
             last_viewport_wireframe: false,
-            wireframe_overlay: false,
-            last_viewport_wireframe_overlay: false,
+            geo_opacity: 1.0,
+            last_viewport_geo_opacity: 1.0,
             render_points: false,
             point_size: 0.02,
             point_color: [1.0, 1.0, 1.0],
@@ -5352,7 +5354,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     || self.last_viewport_active_camera != self.active_camera
                     || self.last_viewport_show_viewport != self.show_viewport
                     || self.last_viewport_wireframe != self.wireframe
-                    || self.last_viewport_wireframe_overlay != self.wireframe_overlay
+                    || self.last_viewport_geo_opacity != self.geo_opacity
                     || self.last_viewport_render_points != self.render_points
                     || self.last_viewport_point_size != self.point_size
                     || self.last_viewport_point_color != self.point_color;
@@ -5372,41 +5374,29 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     let mvp_pivot = (proj * view_mat * model_pivot).to_cols_array_2d();
 
                     // Same draw order as the wgpu pass: bg quad, grid, origin,
-                    // pivot, cube, spheres.
+                    // pivot, cube, spheres. The node geometry (points +
+                    // spheres) carries the Render node's Opacity; scene
+                    // furniture stays opaque.
                     const NO_TINT: [f32; 4] = [0.0; 4];
-                    let mut draws = vec![SceneDraw { mesh: meshes.viewport_bg, mvp, wireframe: false, wire_tint: NO_TINT }];
+                    let geo_opacity = self.geo_opacity.clamp(0.0, 1.0);
+                    let mut draws = vec![SceneDraw { mesh: meshes.viewport_bg, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0 }];
                     if self.viewport().show_grid {
-                        draws.push(SceneDraw { mesh: meshes.grid, mvp, wireframe: false, wire_tint: NO_TINT });
+                        draws.push(SceneDraw { mesh: meshes.grid, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0 });
                     }
                     if self.viewport().show_origin {
-                        draws.push(SceneDraw { mesh: meshes.origin, mvp, wireframe: false, wire_tint: NO_TINT });
+                        draws.push(SceneDraw { mesh: meshes.origin, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0 });
                     }
                     if self.viewport().show_camera_pivot {
-                        draws.push(SceneDraw { mesh: meshes.pivot, mvp: mvp_pivot, wireframe: false, wire_tint: NO_TINT });
+                        draws.push(SceneDraw { mesh: meshes.pivot, mvp: mvp_pivot, wireframe: false, wire_tint: NO_TINT, opacity: 1.0 });
                     }
                     if self.viewport().show_cube {
-                        draws.push(SceneDraw { mesh: meshes.cube, mvp, wireframe: false, wire_tint: NO_TINT });
+                        draws.push(SceneDraw { mesh: meshes.cube, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0 });
                     }
                     if self.render_points && self.point_vertex_count > 0 {
-                        draws.push(SceneDraw { mesh: meshes.points, mvp, wireframe: false, wire_tint: NO_TINT });
+                        draws.push(SceneDraw { mesh: meshes.points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity });
                     }
                     if self.vertex_count_spheres > 0 {
-                        if self.wireframe_overlay {
-                            // Shaded + wireframe: the filled mesh, then its
-                            // wire pass darkened so the edges separate from
-                            // the identical fill (the wire pipeline's depth
-                            // bias keeps the lines above the coplanar fill).
-                            // The tint stays PARTIAL: wires keep a share of
-                            // their world-anchored vertex colors — a flat
-                            // black lattice is azimuthally unmarked (a UV
-                            // sphere's wires self-map every segment step), so
-                            // it perceptually glues the mesh to the camera
-                            // during orbits, dominating the fill's cues.
-                            draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: false, wire_tint: NO_TINT });
-                            draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: true, wire_tint: [0.0, 0.0, 0.0, 0.45] });
-                        } else {
-                            draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: self.wireframe, wire_tint: NO_TINT });
-                        }
+                        draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: self.wireframe, wire_tint: NO_TINT, opacity: geo_opacity });
                     }
                     renderer.stage_scene((sx, sy, cw, ch), draws);
                     }
@@ -5430,7 +5420,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     self.last_viewport_active_camera = self.active_camera.clone();
                     self.last_viewport_show_viewport = self.show_viewport;
                     self.last_viewport_wireframe = self.wireframe;
-                    self.last_viewport_wireframe_overlay = self.wireframe_overlay;
+                    self.last_viewport_geo_opacity = self.geo_opacity;
                     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 c2ad3b4..b2326a1 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -270,7 +270,7 @@ impl State {
         let show_spreadsheet = self.show_spreadsheet;
         let show_playbar = self.show_playbar;
         let wireframe = self.wireframe;
-        let wireframe_overlay = self.wireframe_overlay;
+        let geo_opacity = self.geo_opacity;
         let render_points = self.render_points;
         let point_size = self.point_size;
         let point_color = self.point_color;
@@ -520,9 +520,13 @@ impl State {
         let render_node = find_or_create_subnet(&mut self.fs_root, "Render", "utility", (0.0, 1.0));
         render_node.children.clear();
 
+        // The overlay toggle is retired — drop it from older saves so the
+        // pane doesn't resurrect it.
+        render_node.params.retain(|p| p.name != "Wireframe Overlay");
+
         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, "Wireframe Overlay", "toggle", bool_str(wireframe_overlay), &[], 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);
         ensure_param(render_node, "Point Color", "color", &color_to_hex(point_color), &[], None, None, None);
@@ -530,7 +534,7 @@ impl State {
         for p in render_node.params.iter_mut() {
             match p.name.as_str() {
                 "Show Wireframe" => set_toggle(p, wireframe),
-                "Wireframe Overlay" => set_toggle(p, wireframe_overlay),
+                "Opacity" => p.default = format!("{:.2}", geo_opacity),
                 "Render Points" => set_toggle(p, render_points),
                 "Point Size" => p.default = format!("{:.2}", point_size),
                 "Point Color" => p.default = color_to_hex(point_color),
@@ -669,7 +673,7 @@ impl State {
             for p in &params {
                 match p.name.as_str() {
                     "Show Wireframe" => if let Ok(val) = p.default.parse::<bool>() { self.wireframe = val; }
-                    "Wireframe Overlay" => if let Ok(val) = p.default.parse::<bool>() { self.wireframe_overlay = val; }
+                    "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); }
                     "Point Color" => if let Some(col) = hex_to_color(&p.default) { self.point_color = col; }
diff --git a/src/vk_smoke.rs b/src/vk_smoke.rs
index 8f40f0e..a884233 100644
--- a/src/vk_smoke.rs
+++ b/src/vk_smoke.rs
@@ -474,9 +474,9 @@ fn main() {
                 renderer.stage_scene(
                     pane,
                     vec![
-                        SceneDraw { mesh: bg_mesh, mvp: Mat4::IDENTITY.to_cols_array_2d(), wireframe: false, wire_tint: [0.0; 4] },
-                        SceneDraw { mesh: grid_mesh, mvp, wireframe: false, wire_tint: [0.0; 4] },
-                        SceneDraw { mesh: cube_mesh, mvp, wireframe: false, wire_tint: [0.0; 4] },
+                        SceneDraw { mesh: bg_mesh, mvp: Mat4::IDENTITY.to_cols_array_2d(), wireframe: false, wire_tint: [0.0; 4], opacity: 1.0 },
+                        SceneDraw { mesh: grid_mesh, mvp, wireframe: false, wire_tint: [0.0; 4], opacity: 1.0 },
+                        SceneDraw { mesh: cube_mesh, mvp, wireframe: false, wire_tint: [0.0; 4], opacity: 1.0 },
                     ],
                 );
             }