graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: deleting a node joins the nodes it was wired between
Every sibling wire that named the deleted node takes the name of what the
node read through its Input, so deleting B from A -> B -> C leaves A -> C.
Undone with the deletion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 15 +++++++++++++++
src/app.rs | 36 ++++++++++++++++++++++++++++++++++++
src/main.rs | 40 ++++++++++++++++++++++++++++++++++++++++
3 files changed, 91 insertions(+)
diff --git a/CLAUDE.md b/CLAUDE.md
index 5089732..a48976a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1046,6 +1046,21 @@ page resolver — with each node of a sheet, grid, border and export chain
bypassed in turn — and `bypass_is_one_flag_however_it_is_asked_for` the
three ways of asking.
+### Deleting a node splices it out
+
+`State::delete_node` rewires around the node before it goes (since
+2026-10-01, `app::splice_out`): every sibling wire that named it — an
+`Input` or a second operand, any plain `node` parameter — takes the name
+the deleted node's own `Input` carried, so deleting B from A → B → C
+leaves A → C. Every way of deleting goes through it (Delete over a
+selection, Cut, the node menu, MCP); a selection is deleted highest slot
+first, one splice at a time, so a run of chained nodes leaves its ends
+joined. Nothing is rewired when the node's Input is empty (a generator),
+an expression or hidden — those wires are left naming it, as before —
+and a node is never wired to itself. The rewiring is in the deletion's
+undo step, since a structure step holds the wires of every node it
+touches. `deleting_a_wired_node_connects_its_neighbours` is the test.
+
### Sibling-first inputs and the Switch node
Two pieces added on 2026-09-21 so a node can be BUILT FROM other nodes
diff --git a/src/app.rs b/src/app.rs
index 514ee70..f921f3d 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -752,6 +752,35 @@ pub fn node_wires_at(root: &FsNode, node: &FsNode, frame: i32) -> Vec<(String, S
}
}
+/// Rewire around the child at `slot` of `dir`, ahead of its removal: every
+/// sibling's wire that names it — `Input` or a second operand, any shown,
+/// plain `node` parameter — takes the name the child's own `Input` wire
+/// carries. Nothing changes when that wire is empty, an expression, hidden
+/// or names the child itself, and a sibling is never wired to itself.
+pub(crate) fn splice_out(dir: &mut FsNode, slot: usize) {
+ let Some(gone) = dir.children.get(slot) else { return };
+ let name = gone.name.clone();
+ let Some(upstream) = gone
+ .params
+ .iter()
+ .find(|p| p.name == "Input" && p.kind() == ParamKind::Node && !p.is_expr() && param_visible(&gone.params, &p.show_when))
+ .map(|p| p.text().trim().to_string())
+ .filter(|u| !u.is_empty() && *u != name)
+ else {
+ return;
+ };
+ for (i, sibling) in dir.children.iter_mut().enumerate() {
+ if i == slot || sibling.name == upstream {
+ continue;
+ }
+ for p in sibling.params.iter_mut() {
+ if p.kind() == ParamKind::Node && !p.is_expr() && p.text().trim() == name {
+ p.set_text(upstream.clone());
+ }
+ }
+ }
+}
+
/// [`node_wires_at`] with nothing evaluated: an expression wire names
/// nothing.
pub fn node_wires(node: &FsNode) -> Vec<(String, String)> {
@@ -6669,9 +6698,16 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
(x, y)
}
+ /// Delete the node at `slot` of the current level, splicing it out of
+ /// its chain: every sibling wire that named it is rewired to what it
+ /// read through its `Input`, so deleting B from A → B → C leaves A → C.
+ /// A node with no Input (a generator) leaves those wires as they were.
+ /// The rewiring is part of the same undo step as the deletion, since a
+ /// structure step holds the wires of every node it touches.
pub fn delete_node(&mut self, slot: usize) -> bool {
let len = self.current_dir().children.len();
if slot < len {
+ splice_out(self.current_dir_mut(), slot);
self.current_dir_mut().children.remove(slot);
if let Some(sel_idx) = self.graph().selected_node() {
if sel_idx == slot {
diff --git a/src/main.rs b/src/main.rs
index 1ed5604..313aa28 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8842,6 +8842,46 @@ mod tests {
/// The shell end to end, through the resolver the viewport calls.
// ----- Parameter references and the Switch node (src/geometry.rs) -----
+ /// Deleting a node wired between two splices it out: what read it reads
+ /// what it read, through every wire — Input and second operand alike —
+ /// and the rewiring is undone with the deletion.
+ #[test]
+ fn deleting_a_wired_node_connects_its_neighbours() {
+ let mut state = State::new(false);
+ let wires = |state: &State, name: &str| -> Vec<(String, String)> {
+ let n = state.current_dir().children.iter().find(|c| c.name == name).expect(name);
+ crate::app::node_wires(n)
+ };
+ state.current_dir_mut().children = vec![
+ ref_node("a", "a", "sphere", vec![("Radius", "float", "1")], vec![]),
+ ref_node("b", "b", "transform", vec![("Input", "node", "a")], vec![]),
+ ref_node("c", "c", "transform", vec![("Input", "node", "b")], vec![]),
+ ref_node("d", "d", "boolean", vec![("Input", "node", "c"), ("With", "node", "b")], vec![]),
+ ];
+ state.sync_nodes();
+ state.record_structure_changes();
+
+ assert!(state.delete_node(1));
+ state.record_structure_changes();
+ assert_eq!(wires(&state, "c"), vec![("Input".to_string(), "a".to_string())], "A -> C");
+ assert_eq!(
+ wires(&state, "d"),
+ vec![("Input".to_string(), "c".to_string()), ("With".to_string(), "a".to_string())],
+ "a second operand follows too"
+ );
+
+ // A generator has nothing to splice: what read it is left as it was.
+ assert!(state.delete_node(0));
+ assert_eq!(wires(&state, "c"), vec![("Input".to_string(), "a".to_string())]);
+
+ // Undo puts the node back and the wires with it.
+ state.record_structure_changes();
+ assert!(state.run_command("undo"));
+ assert!(state.run_command("undo"));
+ assert_eq!(wires(&state, "c"), vec![("Input".to_string(), "b".to_string())]);
+ assert_eq!(wires(&state, "d")[1], ("With".to_string(), "b".to_string()));
+ }
+
fn ref_node(id: &str, name: &str, node_type: &str, params: Vec<(&str, &str, &str)>, children: Vec<FsNode>) -> FsNode {
FsNode {
id: id.into(),