graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: Plane Center controls + resolution-control coverage
The Plane already had Rows/Columns resolution spinboxes (the precedent the
Sphere's controls copied); what it lacked for parity was placement — the
kernel hardcoded center (0, 0, 0). Center X/Y/Z become chf() channels like
the Sphere's (scalar, CPU-backend-safe). The new test also covers the
long-standing Rows/Columns tessellation math, which had no coverage.
Existing saved planes gain the controls on load via the template merge.
nodes/plane.json | 17 ++++++++++++++-
src/main.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 80 insertions(+), 1 deletion(-)
diff --git a/nodes/plane.json b/nodes/plane.json
index f5d068c..6c5d35f 100644
--- a/nodes/plane.json
+++ b/nodes/plane.json
@@ -29,6 +29,21 @@
"min": 1,
"max": 128,
"step": 1
+ },
+ {
+ "name": "Center X",
+ "default": "0.0",
+ "type": "slider:-2:2"
+ },
+ {
+ "name": "Center Y",
+ "default": "0.0",
+ "type": "slider:-2:2"
+ },
+ {
+ "name": "Center Z",
+ "default": "0.0",
+ "type": "slider:-2:2"
}
],
"children": [
@@ -38,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 size_x = chf(\"Width\", 1.0f);\n float size_z = chf(\"Length\", 1.0f);\n int cols = chi(\"Columns\", 16);\n int rows = chi(\"Rows\", 16);\n if (cols < 1) cols = 1;\n if (cols > 128) cols = 128;\n if (rows < 1) rows = 1;\n if (rows > 128) rows = 128;\n float center_x = 0.0f;\n float center_y = 0.0f;\n float center_z = 0.0f;\n float half_x = size_x * 0.5f;\n float half_z = size_z * 0.5f;\n float step_x = size_x / (float)cols;\n float step_z = size_z / (float)rows;\n float inv_x = size_x > 0.0f ? 1.0f / size_x : 0.0f;\n float inv_z = size_z > 0.0f ? 1.0f / size_z : 0.0f;\n int count = 0;\n for (int i = 0; i < cols; i++) {\n float x0 = -half_x + (float)i * step_x;\n float x1 = x0 + step_x;\n for (int j = 0; j < rows; j++) {\n float z0 = -half_z + (float)j * step_z;\n float z1 = z0 + step_z;\n float px[6] = {x0, x1, x1, x0, x0, x1};\n float pz[6] = {z0, z1, z0, z0, z1, z1};\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;\n out_pos[idx * 3 + 2] = center_z + pz[v];\n float fx = (px[v] + half_x) * inv_x;\n float fz = (pz[v] + half_z) * inv_z;\n out_col[idx * 3 + 0] = 0.35f + fx * 0.35f;\n out_col[idx * 3 + 1] = 0.45f + fz * 0.35f;\n out_col[idx * 3 + 2] = 0.85f;\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 size_x = chf(\"Width\", 1.0f);\n float size_z = chf(\"Length\", 1.0f);\n int cols = chi(\"Columns\", 16);\n int rows = chi(\"Rows\", 16);\n if (cols < 1) cols = 1;\n if (cols > 128) cols = 128;\n if (rows < 1) rows = 1;\n if (rows > 128) rows = 128;\n float center_x = chf(\"Center X\", 0.0f);\n float center_y = chf(\"Center Y\", 0.0f);\n float center_z = chf(\"Center Z\", 0.0f);\n float half_x = size_x * 0.5f;\n float half_z = size_z * 0.5f;\n float step_x = size_x / (float)cols;\n float step_z = size_z / (float)rows;\n float inv_x = size_x > 0.0f ? 1.0f / size_x : 0.0f;\n float inv_z = size_z > 0.0f ? 1.0f / size_z : 0.0f;\n int count = 0;\n for (int i = 0; i < cols; i++) {\n float x0 = -half_x + (float)i * step_x;\n float x1 = x0 + step_x;\n for (int j = 0; j < rows; j++) {\n float z0 = -half_z + (float)j * step_z;\n float z1 = z0 + step_z;\n float px[6] = {x0, x1, x1, x0, x0, x1};\n float pz[6] = {z0, z1, z0, z0, z1, z1};\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;\n out_pos[idx * 3 + 2] = center_z + pz[v];\n float fx = (px[v] + half_x) * inv_x;\n float fz = (pz[v] + half_z) * inv_z;\n out_col[idx * 3 + 0] = 0.35f + fx * 0.35f;\n out_col[idx * 3 + 1] = 0.45f + fz * 0.35f;\n out_col[idx * 3 + 2] = 0.85f;\n }\n }\n }\n }\n *out_count = count;\n }\n}"
}
],
"position": [
diff --git a/src/main.rs b/src/main.rs
index bdefc2c..e4bacb4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1131,6 +1131,70 @@ mod tests {
assert!(geom.vertices.iter().all(|v| !v.attributes.contains_key("mass")));
}
+ /// The Plane template's construction controls: Rows/Columns set the grid
+ /// tessellation (vertex count = rows * columns * 6 — coverage the
+ /// long-standing spinboxes never had), and the Center X/Y/Z channels
+ /// place the plane like the Sphere's do.
+ #[test]
+ fn test_plane_construction_controls() {
+ let templates_root = crate::app::load_fs_tree();
+ let plane_t = templates_root.children.iter().find(|t| t.name == "Plane").unwrap();
+ let build = |params: &[(&str, &str)]| {
+ let mut inst = plane_t.clone();
+ inst.id = "p".to_string();
+ inst.name = "Plane 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("plane generation failed");
+ assert!(err.is_none(), "{err:?}");
+ geom
+ };
+
+ // Defaults: a 16x16 grid at the origin, flat on y = 0.
+ let base = build(&[]);
+ assert_eq!(base.vertices.len(), 16 * 16 * 6);
+ assert!(base.vertices.iter().all(|v| v.pos[1].abs() < 1e-6));
+
+ // Resolution: 3 columns x 2 rows.
+ assert_eq!(build(&[("Rows", "2"), ("Columns", "3")]).vertices.len(), 3 * 2 * 6);
+
+ // Center: lifts to y = 0.3 and shifts x by 1 (span [0.5, 1.5]).
+ let moved = build(&[("Center X", "1.0"), ("Center Y", "0.3")]);
+ let (mut min_x, mut max_x) = (f32::MAX, f32::MIN);
+ for v in &moved.vertices {
+ assert!((v.pos[1] - 0.3).abs() < 1e-5);
+ 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}");
+ }
+
/// The loader's template merge: saved instances gain params their
/// template grew after the save (values they already hold are kept), a
/// subnet instance's kernel refreshes to the template's (so the new