graphic design tool
git clone https://git.lucas.co/cce-designer.git
fix: Scatter evaluates when dispatched — drop the double cycle guard
generate_single_node_geometry_with_errors pushes the target id before
dispatching, so the scatter resolver's own visited guard saw it and refused
every dispatched call: the spreadsheet of a selected Scatter came up empty
and any downstream consumer ate None — while the scene walk's direct call
(fresh visited) kept the node looking healthy. Cycles stay guarded by the
dispatch itself; the resolver-local pop that would now underflow the
dispatch's stack entry is gone too. Chain test: Sphere → Scatter →
Attribute sees the same points the direct eval yields.
src/geometry.rs | 29 ++++++----------------
src/main.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 83 insertions(+), 21 deletions(-)
diff --git a/src/geometry.rs b/src/geometry.rs
index efebd47..8062a2e 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -876,30 +876,18 @@ pub fn resolve_scatter_geometry_with_errors(
ocl_error: &mut Option<String>,
sim: &mut EvalSim,
) -> Option<Geometry> {
- if visited.contains(&target.id) {
- return None;
- }
- visited.push(target.id.clone());
-
+ // No visited guard here: `generate_single_node_geometry_with_errors`
+ // pushes the target's id before dispatching to this resolver, so a local
+ // `visited.contains` check refused every dispatched call — scatter
+ // geometry evaluated to None for the spreadsheet and for any downstream
+ // consumer, while the scene walk's direct call (fresh `visited`) kept the
+ // node LOOKING healthy. Cycles stay guarded by the dispatch itself.
let input_name = node_param_str(target, "Input", "");
if input_name.is_empty() {
- visited.pop();
return None;
}
- let input_node = match find_node_by_name(root, &input_name) {
- Some(node) => node,
- None => {
- visited.pop();
- return None;
- }
- };
- let geom = match generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim) {
- Some(g) => g,
- None => {
- visited.pop();
- return None;
- }
- };
+ let input_node = find_node_by_name(root, &input_name)?;
+ let geom = generate_single_node_geometry_with_errors(root, input_node, visited, ocl_error, sim)?;
let num_points = node_param_f32(target, "Points", 100.0) as usize;
let radius = node_param_f32(target, "Radius", 0.02);
@@ -976,7 +964,6 @@ pub fn resolve_scatter_geometry_with_errors(
scattered_geom
};
- visited.pop();
Some(res)
}
diff --git a/src/main.rs b/src/main.rs
index dccb4f1..4c96c09 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1131,6 +1131,81 @@ mod tests {
assert!(geom.vertices.iter().all(|v| !v.attributes.contains_key("mass")));
}
+ /// 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
+ /// vanished from any chain while displaying fine on its own.
+ #[test]
+ fn test_scatter_consumed_downstream() {
+ let templates_root = crate::app::load_fs_tree();
+ let find = |name: &str| {
+ templates_root.children.iter().find(|t| t.name == name).unwrap()
+ };
+ let instance = |template: &FsNode, id: &str, name: &str, params: &[(&str, &str)]| {
+ let mut inst = template.clone();
+ inst.id = id.to_string();
+ inst.name = name.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();
+ }
+ inst
+ };
+ let root = FsNode {
+ id: "root".to_string(),
+ name: "root".to_string(),
+ node_type: "node".to_string(),
+ children: vec![
+ instance(find("Sphere"), "s", "Sphere 1", &[]),
+ instance(find("Scatter"), "sc", "Scatter 1", &[("Input", "Sphere 1")]),
+ instance(find("Attribute"), "a", "Attr 1", &[
+ ("Input", "Scatter 1"),
+ ("Operation", "Create"),
+ ("Attribute Name", "mass"),
+ ("Value", "1.00"),
+ ]),
+ ],
+ params: vec![],
+ geometry_visible: true,
+ position: (0.0, 0.0),
+ inputs: 0,
+ outputs: 0,
+ };
+
+ // The scatter alone works (the scene walk's direct-call path)…
+ let mut visited = Vec::new();
+ let mut err = None;
+ let mut cache = crate::geometry::SimCache::default();
+ let direct = crate::geometry::generate_single_node_geometry_with_errors(
+ &root,
+ &root.children[1],
+ &mut visited,
+ &mut err,
+ &mut crate::geometry::EvalSim::new(0, 0, &mut cache),
+ ).expect("scatter evaluates on its own");
+ assert!(err.is_none(), "{err:?}");
+ assert!(!direct.vertices.is_empty());
+
+ // …and the SAME scatter feeding a downstream node yields the SAME
+ // points, tagged by the consumer.
+ let mut visited = Vec::new();
+ let mut err = None;
+ let mut cache = crate::geometry::SimCache::default();
+ let chained = crate::geometry::generate_single_node_geometry_with_errors(
+ &root,
+ &root.children[2],
+ &mut visited,
+ &mut err,
+ &mut crate::geometry::EvalSim::new(0, 0, &mut cache),
+ ).expect("a node consuming a scatter must see its geometry");
+ assert!(err.is_none(), "{err:?}");
+ assert_eq!(chained.vertices.len(), direct.vertices.len());
+ assert!(chained.vertices.iter().all(|v| v.attributes.contains_key("mass")));
+ }
+
/// 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.