graphic design tool
git clone https://git.lucas.co/cce-designer.git
fix: a merged-in parameter lands at the template's position, not the end
The Sphere's Method heads its template, but an instance saved before it
existed gained it below Color: `merge_params` pushed every missing
parameter to the end. It now inserts after the last template parameter
the instance already has, so an old save shows the same pane as a fresh
node.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 7 +++++--
src/app.rs | 14 ++++++++++++--
src/main.rs | 17 ++++++++++++-----
3 files changed, 29 insertions(+), 9 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index fd8526c..61e6674 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1416,8 +1416,11 @@ Missing referenced templates panic at load.
Saved instances are self-contained copies, but the loader merges template
evolution into them (`merge_template_defs` in `src/app.rs`, run on every
-project deserialization including thumbnails): missing params are appended,
-existing ones keep their value but take the template's UI metadata, and subnet
+project deserialization including thumbnails): missing params are inserted
+where the template puts them (after the last template param the instance
+already has — so the Sphere's Method lands above Radius in an old save, not
+below Color), existing ones keep their value but take the template's UI
+metadata, and subnet
templates (Sphere/Plane/Extrude) refresh their children's `Code` outright —
**the template owns the surface and implementation, the instance owns its
values.** A kernel hand-edited inside a template instance reverts on load;
diff --git a/src/app.rs b/src/app.rs
index 08823a4..c801a6d 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -764,8 +764,17 @@ pub fn merge_template_defs(root: &mut FsNode, templates: &[NodeTemplate]) {
}
}
fn merge_params(node: &mut FsNode, template: &FsNode) {
+ // A missing parameter goes where the TEMPLATE puts it — after the
+ // last template parameter the instance already has — not at the
+ // end. The pane's order is the template's statement of what matters
+ // first (the Sphere's Method sits above the Radius it governs), and
+ // an old save that appended every later control below Color was
+ // showing a different node from a fresh one.
+ let mut cursor = 0;
for tp in &template.params {
- if let Some(ip) = node.params.iter_mut().find(|p| p.name == tp.name) {
+ if let Some(i) = node.params.iter().position(|p| p.name == tp.name) {
+ cursor = i + 1;
+ let ip = &mut node.params[i];
ip.param_type = tp.param_type.clone();
ip.label = tp.label.clone();
ip.options = tp.options.clone();
@@ -779,7 +788,8 @@ pub fn merge_template_defs(root: &mut FsNode, templates: &[NodeTemplate]) {
// irrelevant rows would not hide them there.
ip.show_when = tp.show_when.clone();
} else {
- node.params.push(tp.clone());
+ node.params.insert(cursor, tp.clone());
+ cursor += 1;
}
}
}
diff --git a/src/main.rs b/src/main.rs
index 7f5b611..d44e200 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2875,13 +2875,13 @@ mod tests {
};
crate::app::merge_template_defs(&mut root, &templates);
- // Sphere: new params inserted where the template puts them (Method
- // ahead of the rows it governs, Frequency and Resolution after
- // them) with template defaults, value kept, kernel refreshed.
+ // Sphere: new params inserted where the template puts them — Method
+ // ABOVE the Radius the instance already had, the rest after it —
+ // with template defaults, value kept, kernel refreshed.
let s = &root.children[0];
let names: Vec<&str> = s.params.iter().map(|p| p.name.as_str()).collect();
- assert_eq!(names, ["Radius", "Method", "Rows", "Columns", "Frequency", "Resolution", "Center X", "Center Y", "Center Z", "Color"]);
- assert_eq!(s.params[0].default, "0.70", "instance value survives");
+ assert_eq!(names, ["Method", "Radius", "Rows", "Columns", "Frequency", "Resolution", "Center X", "Center Y", "Center Z", "Color"]);
+ assert_eq!(s.params.iter().find(|p| p.name == "Radius").unwrap().default, "0.70", "instance value survives");
let code = &s.children.iter().find(|c| c.name == "opencl1").unwrap()
.params.iter().find(|p| p.name == "Code").unwrap().default;
assert!(code.contains("chi(\"Rows\""), "kernel refreshed from template");
@@ -3003,6 +3003,13 @@ mod tests {
let method = sphere_t.params.iter().find(|p| p.name == "Method").expect("a Method dropdown");
assert_eq!(method.param_type, "choice:UV,Icosphere,Cube");
assert_eq!(method.default, "UV", "the default stays the sphere every saved project was built with");
+ assert_eq!(sphere_t.params[0].name, "Method", "the method heads the pane, above the radius it governs");
+ // And it heads the pane of a sphere SAVED before it existed too: the
+ // bundled project's sphere1 gains it through the loader's merge, at
+ // the template's position rather than below Color.
+ let state = State::new(false);
+ let saved = state.current_dir().children.iter().find(|c| c.name == "sphere1").expect("the bundled sphere1");
+ assert_eq!(saved.params[0].name, "Method", "merged order: {:?}", saved.params.iter().map(|p| &p.name).collect::<Vec<_>>());
let build = |params: &[(&str, &str)]| {
let mut inst = sphere_t.clone();
inst.id = "s".to_string();