graphic design tool
git clone https://git.lucas.co/cce-designer.git
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 },
],
);
}