graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: Sphere construction controls — Rows, Columns, Center
The template kernel's hardcoded tessellation (16x24) and center (0, 0.55, 0)
become channels: Rows/Columns via chi() spinboxes (kernel-clamped to 2..128
and 3..128, so out-of-range MCP writes degrade instead of emitting nothing;
max tessellation stays under the 200k-vertex launch cap), Center as three
chf() sliders — scalar channels rather than one chv(), because the CPU
reference backend deliberately has no float3 and chv would break every
headless eval. Defaults reproduce the historical sphere byte-identically
(the 2304-vertex baselines still pass). Saved instances are self-contained
— they keep their stored kernel and params, so only newly added Sphere
nodes carry the controls.
nodes/sphere.json | 54 ++++++++++++++++++++++++++++++++++++++++-----
src/main.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 115 insertions(+), 5 deletions(-)
diff --git a/nodes/sphere.json b/nodes/sphere.json
index e92aa72..33ab8ae 100644
--- a/nodes/sphere.json
+++ b/nodes/sphere.json
@@ -4,7 +4,42 @@
"inputs": 0,
"outputs": 1,
"params": [
- { "name": "Radius", "default": "0.5", "type": "slider" }
+ {
+ "name": "Radius",
+ "default": "0.5",
+ "type": "slider"
+ },
+ {
+ "name": "Rows",
+ "default": "16",
+ "type": "spinbox",
+ "min": 2,
+ "max": 128,
+ "step": 1
+ },
+ {
+ "name": "Columns",
+ "default": "24",
+ "type": "spinbox",
+ "min": 3,
+ "max": 128,
+ "step": 1
+ },
+ {
+ "name": "Center X",
+ "default": "0.0",
+ "type": "slider:-2:2"
+ },
+ {
+ "name": "Center Y",
+ "default": "0.55",
+ "type": "slider:-2:2"
+ },
+ {
+ "name": "Center Z",
+ "default": "0.0",
+ "type": "slider:-2:2"
+ }
],
"children": [
{
@@ -13,18 +48,27 @@
"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 = 0.0f;\n float center_y = 0.55f;\n float center_z = 0.0f;\n int lat_steps = 16;\n int lon_steps = 24;\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, x10, x11, x00, x11, x01};\n float py[6] = {y00, y10, y11, y00, y11, y01};\n float pz[6] = {z00, z10, z11, z00, z11, z01};\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 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, x10, x11, x00, x11, x01};\n float py[6] = {y00, y10, y11, y00, y11, y01};\n float pz[6] = {z00, z10, z11, z00, z11, z01};\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}"
}
],
- "position": [4.0, 2.0]
+ "position": [
+ 4.0,
+ 2.0
+ ]
},
{
"name": "output1",
"type": "output",
"params": [
- { "name": "Input", "default": "opencl1" }
+ {
+ "name": "Input",
+ "default": "opencl1"
+ }
],
- "position": [4.0, 3.0]
+ "position": [
+ 4.0,
+ 3.0
+ ]
}
]
}
diff --git a/src/main.rs b/src/main.rs
index 4c96c09..6205b8a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1131,6 +1131,72 @@ mod tests {
assert!(geom.vertices.iter().all(|v| !v.attributes.contains_key("mass")));
}
+ /// The Sphere template's construction controls: Rows/Columns set the
+ /// lat/lon tessellation (vertex count = rows * columns * 6), Center X/Y/Z
+ /// place the sphere, and the defaults keep the historical 16x24 sphere at
+ /// (0, 0.55, 0) byte-identical (the extrude test's 2304-vertex baseline).
+ #[test]
+ fn test_sphere_construction_controls() {
+ let templates_root = crate::app::load_fs_tree();
+ let sphere_t = templates_root.children.iter().find(|t| t.name == "Sphere").unwrap();
+ let build = |params: &[(&str, &str)]| {
+ let mut inst = sphere_t.clone();
+ inst.id = "s".to_string();
+ inst.name = "Sphere 1".to_string();
+ for child in &mut inst.children {
+ child.id = format!("{}_{}", inst.id, child.name);
+ }
+ for (pname, val) in params {
+ inst.params.iter_mut().find(|p| p.name == *pname).unwrap().default =
+ val.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 mut cache = crate::geometry::SimCache::default();
+ 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 cache),
+ ).expect("sphere generation failed");
+ assert!(err.is_none(), "{err:?}");
+ geom
+ };
+
+ // Defaults: the historical 16x24 sphere.
+ assert_eq!(build(&[]).vertices.len(), 16 * 24 * 6);
+
+ // A coarse 4x6 tessellation.
+ let coarse = build(&[("Rows", "4"), ("Columns", "6")]);
+ assert_eq!(coarse.vertices.len(), 4 * 6 * 6);
+
+ // Center X shifts the whole sphere: default spans x in [-0.5, 0.5],
+ // shifted spans [0.5, 1.5].
+ let shifted = build(&[("Center X", "1.0")]);
+ let (mut min_x, mut max_x) = (f32::MAX, f32::MIN);
+ for v in &shifted.vertices {
+ min_x = min_x.min(v.pos[0]);
+ max_x = max_x.max(v.pos[0]);
+ }
+ assert!((min_x - 0.5).abs() < 0.01, "min x {min_x}");
+ assert!((max_x - 1.5).abs() < 0.01, "max x {max_x}");
+
+ // Degenerate resolutions clamp instead of emitting nothing.
+ assert_eq!(build(&[("Rows", "0"), ("Columns", "0")]).vertices.len(), 2 * 3 * 6);
+ }
+
/// A Scatter consumed downstream must still evaluate: the dispatch pushes
/// the target id before dispatching, so a resolver-local visited guard
/// sees it and refuses every dispatched call — scatter geometry silently