git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commit423268f55c202f5f0729cffb06ccc437280f03e9
parentbe84093a9f
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-30 12:38
refactor: Visualize's Blend has no Mix; a save holding it loads as Set

Mix fell into Set's arm: Opacity fades every blend alike, so the two
were one option under two names. The merge turns a saved Mix into Set so
the row does not load as an invalid choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 nodes/visualize.json |  4 ++--
 src/app.rs           |  8 ++++++++
 src/geometry.rs      |  5 +++--
 src/main.rs          | 23 +++++++++++++++++++++++
 4 files changed, 36 insertions(+), 4 deletions(-)

diff --git a/nodes/visualize.json b/nodes/visualize.json
index f8d1db8..6c05f59 100644
--- a/nodes/visualize.json
+++ b/nodes/visualize.json
@@ -52,10 +52,10 @@
     },
     {
       "name": "Blend",
-      "type": "choice:Set,Mix,Multiply,Add",
+      "type": "choice:Set,Multiply,Add",
       "default": "Set",
       "show_when": "Mode == Ramp",
-      "description": "How the ramp colour meets the existing colour: Set and Mix replace it, Multiply tints it, Add brightens it. Opacity then fades toward the result."
+      "description": "How the ramp colour meets the existing colour: Set replaces it, Multiply tints it, Add brightens it. Opacity then fades toward the result."
     },
     {
       "name": "Opacity",
diff --git a/src/app.rs b/src/app.rs
index 503e6b0..4c31883 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1135,6 +1135,14 @@ pub fn merge_template_defs(root: &mut FsNode, templates: &[NodeTemplate]) {
         }
     }
     fn merge_node(node: &mut FsNode, templates: &[NodeTemplate]) {
+        // Visualize's Mix blend was Set under another name (Opacity fades
+        // every blend alike) and is retired; a save holding it is Set, or
+        // it would load as a choice the row no longer offers.
+        if node.node_type == "visualize" {
+            if let Some(p) = node.params.iter_mut().find(|p| p.name == "Blend" && p.text().trim().eq_ignore_ascii_case("mix")) {
+                p.set_text("Set".to_string());
+            }
+        }
         if let Some(t) = template_for(node, templates) {
             let owns_impl = t.node_type.eq_ignore_ascii_case("node") && !t.children.is_empty();
             let children_match = t.children.iter().all(|tc| {
diff --git a/src/geometry.rs b/src/geometry.rs
index f79339f..9e84663 100644
--- a/src/geometry.rs
+++ b/src/geometry.rs
@@ -3869,8 +3869,9 @@ pub(crate) fn apply_visualize(geom: &mut Detail, target: &FsNode, ocl_error: &mu
             _ => c,
         };
         // Opacity is applied the same way for every blend, so a stack of
-        // Visualize nodes fades uniformly and Mix is just Set at less than
-        // full strength.
+        // Visualize nodes fades uniformly. (A Mix blend was Set by another
+        // name, this fade being all it did; it is retired, and a save
+        // holding it loads as Set.)
         let out = [
             old[0] + (mixed[0] - old[0]) * opacity,
             old[1] + (mixed[1] - old[1]) * opacity,
diff --git a/src/main.rs b/src/main.rs
index 4c2823f..2bc4d56 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4967,6 +4967,29 @@ mod tests {
             .expect("a known kind is added");
     }
 
+    /// Visualize's Blend has no Mix: it was Set under another name, Opacity
+    /// fading every blend alike. A save that chose it loads as Set, a
+    /// valid choice, rather than as a text the row no longer offers.
+    #[test]
+    fn visualize_mix_blend_loads_as_set() {
+        let templates_root = crate::app::load_fs_tree();
+        let templates = crate::app::flatten_node_templates(&templates_root);
+        let t = templates_root.children.iter().find(|t| t.node_type == "visualize").unwrap();
+        let blend = t.params.iter().find(|p| p.name == "Blend").unwrap();
+        assert_eq!(blend.choice_options(), vec!["Set", "Multiply", "Add"]);
+        let mut old = t.clone();
+        let p = old.params.iter_mut().find(|p| p.name == "Blend").unwrap();
+        p.set_type("choice:Set,Mix,Multiply,Add");
+        p.set_text("Mix".to_string());
+        assert!(p.invalid().is_none(), "the old row took Mix");
+        let mut root = templates_root.clone();
+        root.children = vec![old];
+        crate::app::merge_template_defs(&mut root, &templates);
+        let p = root.children[0].params.iter().find(|p| p.name == "Blend").unwrap();
+        assert_eq!(p.text(), "Set");
+        assert!(p.invalid().is_none(), "{:?}", p.invalid());
+    }
+
     #[test]
     fn test_loader_merges_new_template_params() {
         let templates_root = crate::app::load_fs_tree();