graphic design tool
git clone https://git.lucas.co/cce-designer.git
fix: a node inside a simnet is read as the scene shows it
The spreadsheet, the markers on its selected rows and the selected
group's markers evaluated the node bare, so inside a simnet they read
the seed at every frame and stood still through a playback.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLAUDE.md | 11 +++++++++++
src/app.rs | 6 ++----
src/geometry.rs | 40 ++++++++++++++++++++++++++++++++++++++++
src/main.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 107 insertions(+), 4 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index b224e4e..d5d69ed 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1634,6 +1634,17 @@ wears and at Group Marker Size.
`selected_spreadsheet_rows_are_marked_in_the_scene` drives it by pointer.
+**What reads a selected node for display reads it as the scene shows it**
+(`geometry::node_geometry_as_shown`, the same day): the spreadsheet's
+rows, the markers on them and the selected group's. A node inside a
+simnet is evaluated as the frame's last substep saw it, the feedback of
+the nearest simnet above it pushed — the rule the dived-in scene walk and
+the pull arrows already drew by. Until then these evaluated the node
+bare, so inside a simnet `input` read the seed and the rows, and a
+selected row's marker, stood at the first frame while the scene beside
+them played. `rows_selected_inside_a_simnet_follow_the_simulation` is the
+test.
+
### The volume representation
`src/volume.rs` is a dense signed distance field — `Volume { origin, voxel,
diff --git a/src/app.rs b/src/app.rs
index 8b21991..35a13d1 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -6312,10 +6312,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
let mut points: Vec<[f32; 3]> = Vec::new();
if let Some(node) = selected_node {
- let mut visited = Vec::new();
let mut ocl_error = None;
let mut sim = crate::geometry::EvalSim::new(sim_frame, sim_start, &mut sim_cache);
- if let Some(geom) = generate_single_node_geometry_with_errors(&self.fs_root, node, &mut visited, &mut ocl_error, &mut sim) {
+ if let Some(geom) = crate::geometry::node_geometry_as_shown(&self.fs_root, node, &mut ocl_error, &mut sim) {
let (h, r) = Self::geometry_to_spreadsheet_data(&geom);
headers = h;
rows = r;
@@ -6345,10 +6344,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
let mut member_verts = Vec::new();
if let Some(node) = selected_node.filter(|n| n.node_type.eq_ignore_ascii_case("group")) {
let group_name = node_param_str(node, "Group Name", "group1");
- let mut visited = Vec::new();
let mut ocl_error = None;
let mut sim = crate::geometry::EvalSim::new(sim_frame, sim_start, &mut sim_cache);
- if let Some(geom) = generate_single_node_geometry_with_errors(&self.fs_root, node, &mut visited, &mut ocl_error, &mut sim) {
+ if let Some(geom) = crate::geometry::node_geometry_as_shown(&self.fs_root, node, &mut ocl_error, &mut sim) {
member_verts = crate::geometry::group_member_positions(&geom, &group_name);
}
}
diff --git a/src/geometry.rs b/src/geometry.rs
index 0d0b863..b8fbcf2 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -3507,6 +3507,46 @@ pub fn moves_points(node: &FsNode) -> bool {
&& node_param_str(node, "Attribute Name", "").trim().eq_ignore_ascii_case("Pos")
}
+/// `target`'s geometry as the scene SHOWS it: a node inside a simnet as the
+/// current frame's last substep saw it, with the feedback stack holding the
+/// state that substep consumed, and any other node as it evaluates.
+///
+/// What reads a selected node for display goes through here — the
+/// spreadsheet's rows, the markers on them, the selected group's. Until
+/// 2026-09-29 they evaluated the node bare, so inside a simnet the `input`
+/// child read the simnet's seed and the rows showed the first frame at
+/// every frame, while the scene beside them played. The simnet is the
+/// nearest one above the node, so a node in a subnet inside a simnet is
+/// read the same way.
+pub fn node_geometry_as_shown(
+ root: &FsNode,
+ target: &FsNode,
+ ocl_error: &mut Option<String>,
+ sim: &mut EvalSim,
+) -> Option<Detail> {
+ let mut simnet = None;
+ let mut at = target;
+ while let Some(parent) = find_parent_node(root, &at.id) {
+ if parent.node_type.eq_ignore_ascii_case("simnet") {
+ simnet = Some(parent);
+ break;
+ }
+ at = parent;
+ }
+ let mut pushed = false;
+ if let Some(simnet) = simnet {
+ if let Some(fed) = simnet_step_feedback(root, simnet, &mut Vec::new(), ocl_error, sim) {
+ sim.feedback.push((simnet.id.clone(), fed));
+ pushed = true;
+ }
+ }
+ let geom = generate_single_node_geometry_with_errors(root, target, &mut Vec::new(), ocl_error, sim);
+ if pushed {
+ sim.feedback.pop();
+ }
+ geom
+}
+
/// Where `target` moves each point it moves, as `(before, after)` positions:
/// its input's `P` against its own. Measured rather than read off Value, so
/// Set and Multiply — whose vector differs point to point — and an
diff --git a/src/main.rs b/src/main.rs
index ec21db0..381a946 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -15263,6 +15263,60 @@ mod tests {
assert!(state.row_marker_verts.is_empty());
}
+ /// A node INSIDE a simnet is read as the scene draws it there: as the
+ /// frame's last substep saw it, not from the seed. The spreadsheet's
+ /// rows, and the markers on the rows selected, follow the simulation.
+ #[test]
+ fn rows_selected_inside_a_simnet_follow_the_simulation() {
+ let mut state = State::new(false);
+ state.resize(1600.0, 900.0, 1.0);
+ state.rebuild_positions();
+ state.apply_layout();
+ state.focused_pane = LEFT_MENUBAR_IDX;
+ state.param_editor = crate::slots::CONTENT_IDX;
+ state.show_spreadsheet = true;
+ let mut redraw = false;
+ state.apply_action(McpAction::AddNode { template_name: "Simnet".into(), name: Some("sim".into()), x: 6.0, y: 8.0 }, &mut redraw).unwrap();
+ let sim = state.current_dir().children.iter().position(|c| c.name == "sim").unwrap();
+ state.apply_action(McpAction::SetParam { slot: sim, name: "Input".into(), value: "sphere1".into() }, &mut redraw).unwrap();
+ {
+ let simnet = &mut state.current_dir_mut().children[sim];
+ let mut pull = crate::app::load_fs_tree().children.into_iter().find(|t| t.node_type == "attribute").unwrap();
+ pull.id = "pull-in-sim".into();
+ pull.name = "pull1".into();
+ for (name, value) in [("Input", "input1"), ("Operation", "Modify"), ("Attribute Name", "Pos"), ("Value", "0.05:0.00:0.00"), ("Combine", "Add")] {
+ pull.params.iter_mut().find(|p| p.name == name).unwrap().set_text(value.to_string());
+ }
+ simnet.children.push(pull);
+ let output = simnet.children.iter_mut().find(|c| c.node_type == "output").unwrap();
+ output.params.iter_mut().find(|p| p.name == "Input").unwrap().set_text("pull1".to_string());
+ }
+ // Dive in and select the pull.
+ state.current_path.push(sim);
+ state.sync_nodes();
+ let pull = state.current_dir().children.iter().position(|c| c.name == "pull1").unwrap();
+ state.apply_action(McpAction::Select { slot: pull }, &mut redraw).unwrap();
+ state.slots.playbar.inner_mut().current_frame = 5.0;
+ state.tick_frame(1.0 / 60.0);
+ state.sync_nodes();
+ assert!(!state.spreadsheet_points.is_empty());
+ state.spreadsheet_mut().set_selected_rows(&[3]);
+ state.rebuild_row_marker_verts();
+ let middle = |state: &State| {
+ let n = state.row_marker_verts.len() as f32;
+ state.row_marker_verts.iter().fold(0.0f32, |m, v| m + v.position[0] / n)
+ };
+ let (row_at, marker_at) = (state.spreadsheet_points[3][0], middle(&state));
+ assert!((row_at - marker_at).abs() < 1e-3);
+
+ state.slots.playbar.inner_mut().current_frame = 15.0;
+ state.tick_frame(1.0 / 60.0);
+ let moved = state.spreadsheet_points[3][0] - row_at;
+ assert!(moved > 0.3, "ten frames of the pull moved the row's point {moved}");
+ assert!((middle(&state) - state.spreadsheet_points[3][0]).abs() < 1e-3, "and its marker with it");
+ assert_eq!(state.selected_spreadsheet_points(), vec![3]);
+ }
+
/// The spreadsheet and the selected-group markers evaluate through the
/// SHARED sim cache: with either reading something downstream of a
/// simnet, a refresh costs no steps beyond the ones the frame itself