graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: Plane node — grid-geometry subnet template
Mirrors the Sphere subnet (an opencl node feeding an output node): the
kernel generates a 16x16 two-triangle-cell grid on XZ at y = 0, side
length from the Size param (chf), gradient-colored in the sphere's
palette. Triangles wind CCW viewed from +y — the raster scene pipeline
culls back faces (the RT path doesn't), so a backward winding renders
path-traced but vanishes in the viewport.
Co-Authored-By: Claude Fable 5 <[email protected]>
nodes/plane.json | 43 ++++++++++++++++++++++++++++++++++
src/main.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 113 insertions(+)
diff --git a/nodes/plane.json b/nodes/plane.json
new file mode 100644
index 0000000..9abd095
--- /dev/null
+++ b/nodes/plane.json
@@ -0,0 +1,43 @@
+{
+ "name": "Plane",
+ "type": "node",
+ "inputs": 0,
+ "outputs": 1,
+ "params": [
+ {
+ "name": "Size",
+ "default": "1.0",
+ "type": "slider"
+ }
+ ],
+ "children": [
+ {
+ "name": "opencl1",
+ "type": "opencl",
+ "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 = chf(\"Size\", 1.0f);\n float center_x = 0.0f;\n float center_y = 0.0f;\n float center_z = 0.0f;\n int divs = 16;\n float half_size = size * 0.5f;\n float step = size / (float)divs;\n float inv = size > 0.0f ? 1.0f / size : 0.0f;\n int count = 0;\n for (int i = 0; i < divs; i++) {\n float x0 = -half_size + (float)i * step;\n float x1 = x0 + step;\n for (int j = 0; j < divs; j++) {\n float z0 = -half_size + (float)j * step;\n float z1 = z0 + step;\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_size) * inv;\n float fz = (pz[v] + half_size) * inv;\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": [
+ 4.0,
+ 2.0
+ ]
+ },
+ {
+ "name": "output1",
+ "type": "output",
+ "params": [
+ {
+ "name": "Input",
+ "default": "opencl1"
+ }
+ ],
+ "position": [
+ 4.0,
+ 3.0
+ ]
+ }
+ ]
+}
diff --git a/src/main.rs b/src/main.rs
index 43e012c..4f7449b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -226,6 +226,76 @@ mod tests {
assert!((max_dist_2 - 1.0).abs() < 0.01, "Expected radius around 1.0, got {}", max_dist_2);
}
+ /// The Plane template mirrors the Sphere subnet (an opencl node feeding an
+ /// output node); its kernel generates a divs x divs grid on XZ at y = 0,
+ /// with the Size param as the side length.
+ #[test]
+ fn test_plane_subnet_geometry_generation() {
+ let templates_root = crate::app::load_fs_tree();
+ let plane_template = templates_root
+ .children
+ .iter()
+ .find(|t| t.name == "Plane")
+ .expect("Plane template should be loaded");
+
+ assert_eq!(plane_template.children.len(), 2);
+ let opencl1 = plane_template.children.iter().find(|c| c.name == "opencl1").unwrap();
+ assert_eq!(opencl1.node_type, "opencl");
+ let output1 = plane_template.children.iter().find(|c| c.name == "output1").unwrap();
+ assert_eq!(output1.node_type, "output");
+
+ let generate = |size: Option<&str>, id: &str| {
+ let mut inst = plane_template.clone();
+ inst.id = id.to_string();
+ for child in &mut inst.children {
+ child.id = format!("{}_{}", inst.id, child.name);
+ }
+ if let Some(size) = size {
+ inst.params.iter_mut().find(|p| p.name == "Size").unwrap().default = size.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 ocl_err = None;
+ let geom = crate::geometry::generate_single_node_geometry_with_errors(
+ &root,
+ &root.children[0],
+ &mut visited,
+ &mut ocl_err,
+ ).expect("Geometry generation failed");
+ assert!(ocl_err.is_none(), "OpenCL compilation error: {:?}", ocl_err);
+ geom
+ };
+
+ // Default Size 1.0: a 16x16 grid of two-triangle cells, flat at y = 0,
+ // spanning [-0.5, 0.5] on X and Z.
+ let geom = generate(None, "plane_inst");
+ assert_eq!(geom.vertices.len(), 16 * 16 * 6);
+ let mut max_x: f32 = 0.0;
+ let mut max_z: f32 = 0.0;
+ for v in &geom.vertices {
+ assert!(v.pos[1].abs() < 1e-6, "Expected flat plane at y=0, got y={}", v.pos[1]);
+ max_x = max_x.max(v.pos[0].abs());
+ max_z = max_z.max(v.pos[2].abs());
+ }
+ assert!((max_x - 0.5).abs() < 0.01, "Expected half-size 0.5 on X, got {}", max_x);
+ assert!((max_z - 0.5).abs() < 0.01, "Expected half-size 0.5 on Z, got {}", max_z);
+
+ // Size override 2.0 spans [-1, 1].
+ let geom_2 = generate(Some("2.0"), "plane_inst_2");
+ let max_x_2 = geom_2.vertices.iter().map(|v| v.pos[0].abs()).fold(0.0f32, f32::max);
+ assert!((max_x_2 - 1.0).abs() < 0.01, "Expected half-size 1.0 on X, got {}", max_x_2);
+ }
+
#[test]
fn test_dynamic_parameters_parsing_and_preprocessing() {
let code = r#"