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

commit224a0514ef2f3e946c4faec3ff42e89b4112e637
parent787514ae09
authorLucas Galante <[email protected]>
date2026-07-31 14:04
feat: Wire Thickness slider (1-8 px)

The wire pass draws a LINE_LIST edge expansion of the sphere mesh
(sphere_edges, rebuilt alongside it) at the chosen width. Geometry-color
mode now draws raw unlit vertex colors — brighter than the shaded fill,
which is what separates the wires; far-side wires surviving the depth
test read as their own complementary colors (soft x-ray).

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

 src/app.rs      | 55 +++++++++++++++++++++++++++++++++++++++++--------------
 src/project.rs  |  4 ++++
 src/vk_smoke.rs |  6 +++---
 3 files changed, 48 insertions(+), 17 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index 3dcace1..2e3deb0 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -804,6 +804,10 @@ pub struct SceneMeshes {
     pub cube: cce_ui::vk::MeshId,
     pub viewport_bg: cce_ui::vk::MeshId,
     pub spheres: cce_ui::vk::MeshId,
+    /// LINE_LIST edge expansion of `spheres` (vertex pairs per triangle
+    /// edge) — the wire pass draws real line primitives, never
+    /// PolygonMode::LINE (driver-broken; see cce-ui's scene stage).
+    pub sphere_edges: cce_ui::vk::MeshId,
     pub grid: cce_ui::vk::MeshId,
     pub origin: cce_ui::vk::MeshId,
     pub pivot: cce_ui::vk::MeshId,
@@ -990,12 +994,15 @@ pub struct State {
     pub wireframe: bool,
     pub last_viewport_wireframe: bool,
     /// "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).
+    /// 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],
+    /// 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_width: f32,
     /// 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,
@@ -2829,8 +2836,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             last_viewport_wireframe: false,
             wire_single_color: false,
             wire_color: [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_width: 1.0,
             geo_opacity: 1.0,
             last_viewport_geo_opacity: 1.0,
             render_points: false,
@@ -5203,6 +5212,16 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
         if self.spheres_dirty {
             self.spheres_dirty = false;
             renderer.update_mesh(meshes.spheres, bytemuck::cast_slice(&self.rt_sphere_verts));
+            // Edge mesh for the wire pass: each triangle's three edges as
+            // LINE_LIST vertex pairs, carrying the same colors.
+            let mut edges = Vec::with_capacity(self.rt_sphere_verts.len() * 2);
+            for tri in self.rt_sphere_verts.chunks_exact(3) {
+                for (a, b) in [(0, 1), (1, 2), (2, 0)] {
+                    edges.push(tri[a]);
+                    edges.push(tri[b]);
+                }
+            }
+            renderer.update_mesh(meshes.sphere_edges, bytemuck::cast_slice(&edges));
         }
 
         // The Render node's point display: rebuilt whenever the geometry or
@@ -5246,6 +5265,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             cube: renderer.create_mesh(bytemuck::cast_slice(&cube_verts)),
             viewport_bg: renderer.create_mesh(bytemuck::cast_slice(&bg_verts)),
             spheres: renderer.create_mesh(&[]),
+            sphere_edges: renderer.create_mesh(&[]),
             grid: renderer.create_mesh(bytemuck::cast_slice(&grid_verts)),
             origin: renderer.create_mesh(bytemuck::cast_slice(&origin_verts)),
             pivot: renderer.create_mesh(bytemuck::cast_slice(&pivot_verts)),
@@ -5366,6 +5386,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     || 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_wire_width != self.wire_width
                     || self.last_viewport_render_points != self.render_points
                     || self.last_viewport_point_size != self.point_size
                     || self.last_viewport_point_color != self.point_color;
@@ -5390,37 +5411,42 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     // furniture stays opaque.
                     const NO_TINT: [f32; 4] = [0.0; 4];
                     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 }];
+                    let mut draws = vec![SceneDraw { mesh: meshes.viewport_bg, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0 }];
                     if self.viewport().show_grid {
-                        draws.push(SceneDraw { mesh: meshes.grid, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0 });
+                        draws.push(SceneDraw { mesh: meshes.grid, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0 });
                     }
                     if self.viewport().show_origin {
-                        draws.push(SceneDraw { mesh: meshes.origin, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0 });
+                        draws.push(SceneDraw { mesh: meshes.origin, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0 });
                     }
                     if self.viewport().show_camera_pivot {
-                        draws.push(SceneDraw { mesh: meshes.pivot, mvp: mvp_pivot, wireframe: false, wire_tint: NO_TINT, opacity: 1.0 });
+                        draws.push(SceneDraw { mesh: meshes.pivot, mvp: mvp_pivot, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0 });
                     }
                     if self.viewport().show_cube {
-                        draws.push(SceneDraw { mesh: meshes.cube, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0 });
+                        draws.push(SceneDraw { mesh: meshes.cube, mvp, wireframe: false, wire_tint: NO_TINT, opacity: 1.0, line_width: 1.0, wire_base_width: 0.0 });
                     }
                     if self.render_points && self.point_vertex_count > 0 {
-                        draws.push(SceneDraw { mesh: meshes.points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity });
+                        draws.push(SceneDraw { mesh: meshes.points, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity, line_width: 1.0, wire_base_width: 0.0 });
                     }
                     if self.vertex_count_spheres > 0 {
-                        draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity });
+                        // With wires coming, the fill is pushed back by its
+                        // slope-scaled offset so the lattice reads solid.
+                        let base = if self.wireframe { self.wire_width } else { 0.0 };
+                        draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: false, wire_tint: NO_TINT, opacity: geo_opacity, line_width: 1.0, wire_base_width: base });
                         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).
+                            // draws the raw vertex colors — the wire pass is
+                            // unlit, so they read brighter than the shaded
+                            // fill beneath. Far-side wires that clear the
+                            // depth test near the limb show as their own
+                            // (complementary) colors — a soft x-ray read.
                             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]
+                                [0.0, 0.0, 0.0, 0.0]
                             };
-                            draws.push(SceneDraw { mesh: meshes.spheres, mvp, wireframe: true, wire_tint: tint, opacity: geo_opacity });
+                            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 });
                         }
                     }
                     renderer.stage_scene((sx, sy, cw, ch), draws);
@@ -5448,6 +5474,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
                     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_wire_width = self.wire_width;
                     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 c226c9f..aed6924 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -272,6 +272,7 @@ impl State {
         let wireframe = self.wireframe;
         let wire_single_color = self.wire_single_color;
         let wire_color = self.wire_color;
+        let wire_width = self.wire_width;
         let geo_opacity = self.geo_opacity;
         let render_points = self.render_points;
         let point_size = self.point_size;
@@ -530,6 +531,7 @@ impl State {
         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, "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);
         ensure_param(render_node, "Point Size", "slider:0.000:0.100:3", &format!("{:.3}", point_size), &[], Some(0.0), Some(0.10), None);
@@ -540,6 +542,7 @@ impl State {
                 "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 Thickness" => p.default = format!("{:.1}", wire_width),
                 "Opacity" => p.default = format!("{:.2}", geo_opacity),
                 "Render Points" => set_toggle(p, render_points),
                 "Point Size" => {
@@ -688,6 +691,7 @@ impl State {
                     "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 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; }
                     "Point Size" => if let Ok(val) = p.default.parse::<f32>() { self.point_size = val.clamp(0.0, 0.1); }
diff --git a/src/vk_smoke.rs b/src/vk_smoke.rs
index a884233..90a554d 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], 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 },
+                        SceneDraw { mesh: bg_mesh, mvp: Mat4::IDENTITY.to_cols_array_2d(), wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0 },
+                        SceneDraw { mesh: grid_mesh, mvp, wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0 },
+                        SceneDraw { mesh: cube_mesh, mvp, wireframe: false, wire_tint: [0.0; 4], opacity: 1.0, line_width: 1.0, wire_base_width: 0.0 },
                     ],
                 );
             }