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

commitd1a0282c06e50f518ad442196c2882602701eba8
parent58547ba728
authorLucas Galante <[email protected]>
date2026-08-25 11:02
feat: Guides Point Marker Color

The meta Point Markers overlay's color joins its size in the Guides node —
a hex color param (the Grid Color pattern) driving the previously
hardcoded tint through State::meta_marker_color, linearized at vertex
build; a change re-collects the overlays immediately. Existing metas and
saved Guides nodes gain the param through the usual ensure migration.

 src/app.rs     |  4 ++++
 src/main.rs    | 16 +++++++++++++---
 src/project.rs |  9 +++++++++
 src/render.rs  | 10 ++++++----
 4 files changed, 32 insertions(+), 7 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index d962c90..c88fc47 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1096,6 +1096,9 @@ pub struct State {
     /// World-unit radius of the meta "Point Markers" overlay — the Guides
     /// subnet's "Point Marker Size" control (stored there in thousandths).
     pub meta_marker_size: f32,
+    /// sRGB color of the meta "Point Markers" overlay — the Guides subnet's
+    /// "Point Marker Color" control (stored there as hex, like Grid Color).
+    pub meta_marker_color: [f32; 3],
     /// The raster scene's model-view-projection and the viewport pane rect in
     /// LOGICAL px, cached at staging so the 2D pass can project 3D overlays.
     pub last_scene_mvp: Option<Mat4>,
@@ -3278,6 +3281,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Geometry) -> (Vec<String>, Vec
             meta_normal_verts: Vec::new(),
             meta_normal_count: 0,
             meta_marker_size: 0.02,
+            meta_marker_color: [0.85, 0.85, 1.0],
             last_scene_mvp: None,
             last_scene_view_rect: (0.0, 0.0, 0.0, 0.0),
             last_viewport_rt_mode: false,
diff --git a/src/main.rs b/src/main.rs
index d1b6acd..55106ad 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -523,9 +523,16 @@ mod tests {
                 .expect("Guides has Point Marker Size");
             assert_eq!(p.default, "20", "default = 0.02 world units");
             p.default = "50".to_string();
+            let c = guides.params.iter_mut().find(|p| p.name == "Point Marker Color")
+                .expect("Guides has Point Marker Color");
+            assert_eq!(c.param_type, "color");
+            c.default = "#ff8000".to_string();
         }
         state.apply_settings_from_menubar_subnets();
         assert!((state.meta_marker_size - 0.05).abs() < 1e-6);
+        assert!((state.meta_marker_color[0] - 1.0).abs() < 0.01);
+        assert!((state.meta_marker_color[1] - 0.5).abs() < 0.01);
+        assert!((state.meta_marker_color[2] - 0.0).abs() < 0.01);
     }
 
     /// An old save carries Main/View/Guides/Render at the root with the user's
@@ -1235,7 +1242,7 @@ mod tests {
         // Overlays: nothing while the prefs are off…
         let mut cache = crate::geometry::SimCache::default();
         let (markers, labels, wires, normals) = crate::render::collect_meta_overlays(
-            &root, 0.02, &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
+            &root, 0.02, [1.0, 0.5, 0.0], &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
         assert!(markers.is_empty() && labels.is_empty() && wires.is_empty() && normals.is_empty());
 
         // …all four overlays for the flagged sphere: 240 marker verts per
@@ -1250,10 +1257,13 @@ mod tests {
         assert!(crate::app::meta_pref(&root.children[0], "Wireframe"));
         let mut cache = crate::geometry::SimCache::default();
         let (markers, labels, wires, normals) = crate::render::collect_meta_overlays(
-            &root, 0.02, &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
+            &root, 0.02, [1.0, 0.5, 0.0], &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
         assert!(!labels.is_empty() && labels.len() < 16 * 24 * 6);
         assert_eq!(markers.len(), labels.len() * 240);
         assert!(labels.iter().any(|(_, i)| *i > 0));
+        // The marker color parameter flows into the vertices (linearized).
+        let expect = cce_ui::colors::to_linear_rgb([1.0, 0.5, 0.0]);
+        assert!(markers.iter().all(|v| v.color == expect));
         assert_eq!(wires.len(), (16 * 24 * 6 / 3) * 6);
         // Normals: one whisker per distinct point, pointing OUT of the
         // sphere (center (0, 0.55, 0)) — this pins the winding/negation
@@ -1275,7 +1285,7 @@ mod tests {
         root.children[0].geometry_visible = false;
         let mut cache = crate::geometry::SimCache::default();
         let (markers, labels, wires, normals) = crate::render::collect_meta_overlays(
-            &root, 0.02, &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
+            &root, 0.02, [1.0, 0.5, 0.0], &mut crate::geometry::EvalSim::new(0, 0, &mut cache));
         assert!(markers.is_empty() && labels.is_empty() && wires.is_empty() && normals.is_empty());
     }
 
diff --git a/src/project.rs b/src/project.rs
index 5453216..1ed1862 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -691,6 +691,8 @@ impl State {
         // (the Grid Thickness convention): 20 = 0.02 world units.
         let marker_size_seed = ((self.meta_marker_size * 1000.0).round() as i32).to_string();
         ensure_param(guides_node, "Point Marker Size", "spinbox", &marker_size_seed, &[], Some(5.0), Some(100.0), Some(1.0));
+        let marker_color_seed = color_to_hex(self.meta_marker_color);
+        ensure_param(guides_node, "Point Marker Color", "color", &marker_color_seed, &[], None, None, None);
         for p in guides_node.params.iter_mut() {
             match p.name.as_str() {
                 "Show Grid Guide" => set_toggle(p, vp_show_grid),
@@ -842,6 +844,13 @@ impl State {
                             self.rebuild_scene_geometry();
                         }
                     }
+                    "Point Marker Color" => if let Some(col) = hex_to_color(&p.default) {
+                        if col != self.meta_marker_color {
+                            self.meta_marker_color = col;
+                            // Baked into the marker verts, like the radius.
+                            self.rebuild_scene_geometry();
+                        }
+                    }
                     _ => {}
                 }
             }
diff --git a/src/render.rs b/src/render.rs
index c2c5ff0..548f66e 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -858,7 +858,7 @@ impl State {
         let mut sim_cache = std::mem::take(&mut self.sim_cache);
         let (markers, labels, wires, normals) = {
             let mut sim = crate::geometry::EvalSim::new(frame, start, &mut sim_cache);
-            collect_meta_overlays(&self.fs_root, self.meta_marker_size, &mut sim)
+            collect_meta_overlays(&self.fs_root, self.meta_marker_size, self.meta_marker_color, &mut sim)
         };
         self.sim_cache = sim_cache;
         self.meta_marker_verts = markers;
@@ -899,6 +899,7 @@ impl State {
 pub(crate) fn collect_meta_overlays(
     root: &FsNode,
     point_size: f32,
+    marker_color: [f32; 3],
     sim: &mut crate::geometry::EvalSim,
 ) -> (
     Vec<crate::geometry::Vertex3D>,
@@ -915,6 +916,7 @@ pub(crate) fn collect_meta_overlays(
         node: &FsNode,
         parent_visible: bool,
         point_size: f32,
+        marker_color: [f32; 3],
         markers: &mut Vec<crate::geometry::Vertex3D>,
         labels: &mut Vec<([f32; 3], u32)>,
         wires: &mut Vec<crate::geometry::Vertex3D>,
@@ -956,7 +958,7 @@ pub(crate) fn collect_meta_overlays(
                     markers.extend(crate::geometry::points_vertices(
                         &src,
                         point_size,
-                        cce_ui::colors::to_linear_rgb([0.85, 0.85, 1.0]),
+                        cce_ui::colors::to_linear_rgb(marker_color),
                     ));
                 }
                 if want_normals {
@@ -1016,11 +1018,11 @@ pub(crate) fn collect_meta_overlays(
             }
         }
         for c in &node.children {
-            visit(root, c, is_visible, point_size, markers, labels, wires, normals, sim);
+            visit(root, c, is_visible, point_size, marker_color, markers, labels, wires, normals, sim);
         }
     }
     for c in &root.children {
-        visit(root, c, true, point_size, &mut markers, &mut labels, &mut wires, &mut normals, sim);
+        visit(root, c, true, point_size, marker_color, &mut markers, &mut labels, &mut wires, &mut normals, sim);
     }
     // A dense mesh can label tens of thousands of points; the text pass is
     // per-frame, so cap it rather than melt the frame rate.