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

commit84a110579156211ea86a90c310856278836eca66
parent2a334dd0a2
authorLucas Galante <[email protected]>
date2026-09-21 14:47
feat: Sphere gains a Color toggle

On (the default, and what every saved sphere gets on load through the
template merge), the normal-mapped gradient the sphere has always carried.
Off, the kernel writes DEFAULT_COLOR instead — the same grey that geometry
with no Cd renders at, so an uncoloured sphere looks like an uncoloured
anything else rather than like a second colour scheme. Read in the kernel
through chb(), the toggle prefix the box and extrude templates already use.

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

 nodes/sphere.json |  9 ++++++--
 src/main.rs       | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 71 insertions(+), 3 deletions(-)

diff --git a/nodes/sphere.json b/nodes/sphere.json
index c028188..e79e4b4 100644
--- a/nodes/sphere.json
+++ b/nodes/sphere.json
@@ -39,6 +39,11 @@
    "name": "Center Z",
    "default": "0.0",
    "type": "slider:-2:2"
+  },
+  {
+   "name": "Color",
+   "default": "true",
+   "type": "toggle"
   }
  ],
  "children": [
@@ -48,7 +53,7 @@
    "params": [
     {
      "name": "Code",
-     "default": "__kernel void process(__global const float* in_pos, __global const float* in_col, int in_count, __global float* out_pos, __global float* out_col, __global int* out_count, int max_vertices) {\n    int id = get_global_id(0);\n    if (id == 0) {\n        float radius = chf(\"Radius\", 0.5f);\n        float center_x = chf(\"Center X\", 0.0f);\n        float center_y = chf(\"Center Y\", 0.55f);\n        float center_z = chf(\"Center Z\", 0.0f);\n        int lat_steps = chi(\"Rows\", 16);\n        if (lat_steps < 2) { lat_steps = 2; }\n        if (lat_steps > 128) { lat_steps = 128; }\n        int lon_steps = chi(\"Columns\", 24);\n        if (lon_steps < 3) { lon_steps = 3; }\n        if (lon_steps > 128) { lon_steps = 128; }\n        int count = 0;\n        for (int lat = 0; lat < lat_steps; lat++) {\n            float theta0 = 3.14159265f * (float)lat / (float)lat_steps;\n            float theta1 = 3.14159265f * (float)(lat + 1) / (float)lat_steps;\n            for (int lon = 0; lon < lon_steps; lon++) {\n                float phi0 = 6.2831853f * (float)lon / (float)lon_steps;\n                float phi1 = 6.2831853f * (float)(lon + 1) / (float)lon_steps;\n                float x00 = radius * sin(theta0) * cos(phi0);\n                float y00 = radius * cos(theta0);\n                float z00 = radius * sin(theta0) * sin(phi0);\n                float x10 = radius * sin(theta1) * cos(phi0);\n                float y10 = radius * cos(theta1);\n                float z10 = radius * sin(theta1) * sin(phi0);\n                float x11 = radius * sin(theta1) * cos(phi1);\n                float y11 = radius * cos(theta1);\n                float z11 = radius * sin(theta1) * sin(phi1);\n                float x01 = radius * sin(theta0) * cos(phi1);\n                float y01 = radius * cos(theta0);\n                float z01 = radius * sin(theta0) * sin(phi1);\n                float px[6] = {x00, x11, x10, x00, x01, x11};\n                float py[6] = {y00, y11, y10, y00, y01, y11};\n                float pz[6] = {z00, z11, z10, z00, z01, z11};\n                for (int v = 0; v < 6; v++) {\n                    int idx = count++;\n                    if (idx < max_vertices) {\n                        out_pos[idx * 3 + 0] = center_x + px[v];\n                        out_pos[idx * 3 + 1] = center_y + py[v];\n                        out_pos[idx * 3 + 2] = center_z + pz[v];\n                        float nx = px[v];\n                        float ny = py[v];\n                        float nz = pz[v];\n                        float len = sqrt(nx*nx + ny*ny + nz*nz);\n                        if (len > 0.0f) {\n                            nx /= len;\n                            ny /= len;\n                            nz /= len;\n                        }\n                        out_col[idx * 3 + 0] = 0.5f + nx * 0.5f;\n                        out_col[idx * 3 + 1] = 0.5f + ny * 0.5f;\n                        out_col[idx * 3 + 2] = 0.5f + nz * 0.5f;\n                    }\n                }\n            }\n        }\n        *out_count = count;\n    }\n}"
+     "default": "__kernel void process(__global const float* in_pos, __global const float* in_col, int in_count, __global float* out_pos, __global float* out_col, __global int* out_count, int max_vertices) {\n    int id = get_global_id(0);\n    if (id == 0) {\n        float radius = chf(\"Radius\", 0.5f);\n        float center_x = chf(\"Center X\", 0.0f);\n        float center_y = chf(\"Center Y\", 0.55f);\n        float center_z = chf(\"Center Z\", 0.0f);\n        int lat_steps = chi(\"Rows\", 16);\n        if (lat_steps < 2) { lat_steps = 2; }\n        if (lat_steps > 128) { lat_steps = 128; }\n        int lon_steps = chi(\"Columns\", 24);\n        if (lon_steps < 3) { lon_steps = 3; }\n        if (lon_steps > 128) { lon_steps = 128; }\n        int colored = chb(\"Color\", true) > 0.5f ? 1 : 0;\n        int count = 0;\n        for (int lat = 0; lat < lat_steps; lat++) {\n            float theta0 = 3.14159265f * (float)lat / (float)lat_steps;\n            float theta1 = 3.14159265f * (float)(lat + 1) / (float)lat_steps;\n            for (int lon = 0; lon < lon_steps; lon++) {\n                float phi0 = 6.2831853f * (float)lon / (float)lon_steps;\n                float phi1 = 6.2831853f * (float)(lon + 1) / (float)lon_steps;\n                float x00 = radius * sin(theta0) * cos(phi0);\n                float y00 = radius * cos(theta0);\n                float z00 = radius * sin(theta0) * sin(phi0);\n                float x10 = radius * sin(theta1) * cos(phi0);\n                float y10 = radius * cos(theta1);\n                float z10 = radius * sin(theta1) * sin(phi0);\n                float x11 = radius * sin(theta1) * cos(phi1);\n                float y11 = radius * cos(theta1);\n                float z11 = radius * sin(theta1) * sin(phi1);\n                float x01 = radius * sin(theta0) * cos(phi1);\n                float y01 = radius * cos(theta0);\n                float z01 = radius * sin(theta0) * sin(phi1);\n                float px[6] = {x00, x11, x10, x00, x01, x11};\n                float py[6] = {y00, y11, y10, y00, y01, y11};\n                float pz[6] = {z00, z11, z10, z00, z01, z11};\n                for (int v = 0; v < 6; v++) {\n                    int idx = count++;\n                    if (idx < max_vertices) {\n                        out_pos[idx * 3 + 0] = center_x + px[v];\n                        out_pos[idx * 3 + 1] = center_y + py[v];\n                        out_pos[idx * 3 + 2] = center_z + pz[v];\n                        float nx = px[v];\n                        float ny = py[v];\n                        float nz = pz[v];\n                        float len = sqrt(nx*nx + ny*ny + nz*nz);\n                        if (len > 0.0f) {\n                            nx /= len;\n                            ny /= len;\n                            nz /= len;\n                        }\n                        if (colored) {\n                            out_col[idx * 3 + 0] = 0.5f + nx * 0.5f;\n                            out_col[idx * 3 + 1] = 0.5f + ny * 0.5f;\n                            out_col[idx * 3 + 2] = 0.5f + nz * 0.5f;\n                        } else {\n                            out_col[idx * 3 + 0] = 0.8f;\n                            out_col[idx * 3 + 1] = 0.8f;\n                            out_col[idx * 3 + 2] = 0.8f;\n                        }\n                    }\n                }\n            }\n        }\n        *out_count = count;\n    }\n}"
     }
    ],
    "position": [
@@ -71,4 +76,4 @@
    ]
   }
  ]
-}
\ No newline at end of file
+}
diff --git a/src/main.rs b/src/main.rs
index d201c12..eae34b9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1445,6 +1445,69 @@ mod tests {
         );
     }
 
+    /// The Sphere's Color toggle: on, the normal-mapped gradient the sphere
+    /// has always had; off, `DEFAULT_COLOR` — the same grey geometry with no
+    /// `Cd` at all renders at, so an uncoloured sphere looks like an
+    /// uncoloured anything else rather than like a second colour scheme.
+    #[test]
+    fn sphere_color_toggle_switches_between_gradient_and_default() {
+        let templates_root = crate::app::load_fs_tree();
+        let sphere_template = templates_root
+            .children
+            .iter()
+            .find(|t| t.name == "Sphere")
+            .expect("Sphere template should be loaded");
+        let color = sphere_template.params.iter().find(|p| p.name == "Color").expect("a Color param");
+        assert_eq!(color.param_type, "toggle");
+        assert_eq!(color.default, "true", "coloured by default, as it always was");
+
+        let generate = |on: &str| {
+            let mut inst = sphere_template.clone();
+            inst.id = format!("sphere_{on}");
+            for child in &mut inst.children {
+                child.id = format!("{}_{}", inst.id, child.name);
+            }
+            inst.params.iter_mut().find(|p| p.name == "Color").unwrap().default = on.to_string();
+            let root = FsNode {
+                id: "root".to_string(),
+                name: "root".to_string(),
+                node_type: "node".to_string(),
+                children: vec![inst],
+                params: vec![],
+                geometry_visible: true,
+                position: (0.0, 0.0),
+                inputs: 0,
+                outputs: 0,
+            };
+            let mut visited = Vec::new();
+            let mut err = None;
+            let geom = crate::geometry::generate_single_node_geometry_with_errors(
+                &root,
+                &root.children[0],
+                &mut visited,
+                &mut err,
+                &mut crate::geometry::EvalSim::new(0, 0, &mut crate::geometry::SimCache::default()),
+            )
+            .expect("geometry");
+            assert!(err.is_none(), "kernel error: {err:?}");
+            geom
+        };
+
+        let off = generate("false");
+        assert!(off.num_points() > 0);
+        for p in 0..off.num_points() {
+            assert_eq!(off.color(p), crate::detail::DEFAULT_COLOR, "point {p} off");
+        }
+
+        let on = generate("true");
+        assert_eq!(on.num_points(), off.num_points(), "the toggle changes colour, not shape");
+        let first = on.color(0);
+        assert!(
+            (0..on.num_points()).any(|p| on.color(p) != first),
+            "on, the sphere carries its gradient"
+        );
+    }
+
     #[test]
     fn test_sphere_subnet_geometry_generation() {
         let templates_root = crate::app::load_fs_tree();
@@ -2599,7 +2662,7 @@ mod tests {
         // kernel refreshed.
         let s = &root.children[0];
         let names: Vec<&str> = s.params.iter().map(|p| p.name.as_str()).collect();
-        assert_eq!(names, ["Radius", "Rows", "Columns", "Center X", "Center Y", "Center Z"]);
+        assert_eq!(names, ["Radius", "Rows", "Columns", "Center X", "Center Y", "Center Z", "Color"]);
         assert_eq!(s.params[0].default, "0.70", "instance value survives");
         let code = &s.children.iter().find(|c| c.name == "opencl1").unwrap()
             .params.iter().find(|p| p.name == "Code").unwrap().default;