Skip to main content

proof_engine/shader_graph/
serialize.rs

1//! Graph serialization: custom TOML-like text format for saving/loading shader graphs,
2//! with node ID mapping, connection serialization, parameter type tags, and round-trip fidelity.
3
4use std::collections::HashMap;
5use super::nodes::{
6    Connection, DataType, NodeId, NodeType, ParamValue, ShaderGraph, ShaderNode, Socket,
7    SocketDirection,
8};
9
10// ---------------------------------------------------------------------------
11// Errors
12// ---------------------------------------------------------------------------
13
14/// Errors that can occur during serialization or deserialization.
15#[derive(Debug, Clone)]
16pub enum SerializeError {
17    /// A required field is missing.
18    MissingField(String),
19    /// A value could not be parsed.
20    ParseError(String),
21    /// An unknown node type was encountered.
22    UnknownNodeType(String),
23    /// The format is structurally invalid.
24    FormatError(String),
25    /// A referenced node ID does not exist.
26    InvalidNodeId(u64),
27    /// IO error message (since we can't use std::io::Error in Clone).
28    IoError(String),
29}
30
31impl std::fmt::Display for SerializeError {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            SerializeError::MissingField(s) => write!(f, "Missing field: {}", s),
35            SerializeError::ParseError(s) => write!(f, "Parse error: {}", s),
36            SerializeError::UnknownNodeType(s) => write!(f, "Unknown node type: {}", s),
37            SerializeError::FormatError(s) => write!(f, "Format error: {}", s),
38            SerializeError::InvalidNodeId(id) => write!(f, "Invalid node ID: {}", id),
39            SerializeError::IoError(s) => write!(f, "IO error: {}", s),
40        }
41    }
42}
43
44// ---------------------------------------------------------------------------
45// GraphSerializer
46// ---------------------------------------------------------------------------
47
48/// Serializer/deserializer for shader graphs in a custom TOML-like text format.
49///
50/// ## Format
51///
52/// ```text
53/// [graph]
54/// name = "my_shader"
55/// next_id = 42
56///
57/// [[node]]
58/// id = 1
59/// type = "Color"
60/// label = "Base Color"
61/// enabled = true
62/// editor_x = 100.0
63/// editor_y = 200.0
64/// input.0.default = "vec4:1.0,0.0,0.0,1.0"
65/// property.my_key = "float:0.5"
66///
67/// [[connection]]
68/// from = 1:0
69/// to = 3:1
70/// ```
71pub struct GraphSerializer;
72
73impl GraphSerializer {
74    /// Serialize a shader graph to the custom text format.
75    pub fn serialize(graph: &ShaderGraph) -> String {
76        let mut out = String::new();
77
78        // Header
79        out.push_str("[graph]\n");
80        out.push_str(&format!("name = \"{}\"\n", escape_string(&graph.name)));
81        out.push_str(&format!("next_id = {}\n", graph.next_id_counter()));
82        out.push('\n');
83
84        // Nodes (sorted by ID for determinism)
85        let mut node_ids: Vec<NodeId> = graph.node_ids().collect();
86        node_ids.sort_by_key(|id| id.0);
87
88        for nid in &node_ids {
89            let node = match graph.node(nid) {
90                Some(n) => n,
91                None => continue,
92            };
93
94            out.push_str("[[node]]\n");
95            out.push_str(&format!("id = {}\n", node.id.0));
96            out.push_str(&format!("type = \"{}\"\n", node_type_to_string(&node.node_type)));
97            out.push_str(&format!("label = \"{}\"\n", escape_string(&node.label)));
98            out.push_str(&format!("enabled = {}\n", node.enabled));
99            out.push_str(&format!("editor_x = {}\n", format_f32(node.editor_x)));
100            out.push_str(&format!("editor_y = {}\n", format_f32(node.editor_y)));
101
102            // Conditional
103            if let Some(ref var) = node.conditional_var {
104                out.push_str(&format!("conditional_var = \"{}\"\n", escape_string(var)));
105                out.push_str(&format!("conditional_threshold = {}\n",
106                    format_f32(node.conditional_threshold)));
107            }
108
109            // Input defaults
110            for (idx, socket) in node.inputs.iter().enumerate() {
111                if let Some(ref val) = socket.default_value {
112                    out.push_str(&format!("input.{}.default = \"{}\"\n",
113                        idx, serialize_param_value(val)));
114                }
115            }
116
117            // Properties
118            let mut prop_keys: Vec<&String> = node.properties.keys().collect();
119            prop_keys.sort();
120            for key in prop_keys {
121                let val = &node.properties[key];
122                out.push_str(&format!("property.{} = \"{}\"\n",
123                    escape_string(key), serialize_param_value(val)));
124            }
125
126            out.push('\n');
127        }
128
129        // Connections
130        for conn in graph.connections() {
131            out.push_str("[[connection]]\n");
132            out.push_str(&format!("from = {}:{}\n", conn.from_node.0, conn.from_socket));
133            out.push_str(&format!("to = {}:{}\n", conn.to_node.0, conn.to_socket));
134            out.push('\n');
135        }
136
137        out
138    }
139
140    /// Deserialize a shader graph from the custom text format.
141    pub fn deserialize(input: &str) -> Result<ShaderGraph, SerializeError> {
142        let mut graph_name = String::from("untitled");
143        let mut next_id: u64 = 1;
144        let mut nodes: Vec<ShaderNode> = Vec::new();
145        let mut connections: Vec<Connection> = Vec::new();
146
147        let mut current_section = Section::None;
148        let mut current_node: Option<NodeBuilder> = None;
149        let mut current_conn: Option<ConnBuilder> = None;
150
151        for (line_num, raw_line) in input.lines().enumerate() {
152            let line = raw_line.trim();
153
154            // Skip empty lines and comments
155            if line.is_empty() || line.starts_with('#') {
156                continue;
157            }
158
159            // Section headers
160            if line == "[graph]" {
161                flush_node(&mut current_node, &mut nodes)?;
162                flush_conn(&mut current_conn, &mut connections)?;
163                current_section = Section::Graph;
164                continue;
165            }
166            if line == "[[node]]" {
167                flush_node(&mut current_node, &mut nodes)?;
168                flush_conn(&mut current_conn, &mut connections)?;
169                current_section = Section::Node;
170                current_node = Some(NodeBuilder::default());
171                continue;
172            }
173            if line == "[[connection]]" {
174                flush_node(&mut current_node, &mut nodes)?;
175                flush_conn(&mut current_conn, &mut connections)?;
176                current_section = Section::Connection;
177                current_conn = Some(ConnBuilder::default());
178                continue;
179            }
180
181            // Key-value pairs
182            let (key, value) = parse_kv(line)
183                .ok_or_else(|| SerializeError::FormatError(
184                    format!("Invalid line {}: '{}'", line_num + 1, line)))?;
185
186            match current_section {
187                Section::Graph => {
188                    match key.as_str() {
189                        "name" => graph_name = unquote(&value),
190                        "next_id" => next_id = value.parse().map_err(|e| {
191                            SerializeError::ParseError(format!("next_id: {}", e))
192                        })?,
193                        _ => {} // ignore unknown keys
194                    }
195                }
196                Section::Node => {
197                    if let Some(ref mut nb) = current_node {
198                        parse_node_field(nb, &key, &value)?;
199                    }
200                }
201                Section::Connection => {
202                    if let Some(ref mut cb) = current_conn {
203                        parse_conn_field(cb, &key, &value)?;
204                    }
205                }
206                Section::None => {
207                    // Ignore lines before any section
208                }
209            }
210        }
211
212        // Flush remaining
213        flush_node(&mut current_node, &mut nodes)?;
214        flush_conn(&mut current_conn, &mut connections)?;
215
216        // Build graph
217        let mut graph = ShaderGraph::new(&graph_name);
218        graph.set_next_id(next_id);
219
220        for node in nodes {
221            graph.insert_node(node);
222        }
223        for conn in connections {
224            graph.add_connection_raw(conn);
225        }
226
227        Ok(graph)
228    }
229
230    /// Serialize a graph to a string, then deserialize it back.
231    /// Returns the round-tripped graph. Useful for testing fidelity.
232    pub fn round_trip(graph: &ShaderGraph) -> Result<ShaderGraph, SerializeError> {
233        let serialized = Self::serialize(graph);
234        Self::deserialize(&serialized)
235    }
236
237    /// Verify round-trip fidelity: serialize, deserialize, and check equality.
238    pub fn verify_round_trip(graph: &ShaderGraph) -> Result<Vec<String>, SerializeError> {
239        let restored = Self::round_trip(graph)?;
240        let mut diffs = Vec::new();
241
242        // Compare names
243        if graph.name != restored.name {
244            diffs.push(format!("Name mismatch: '{}' vs '{}'", graph.name, restored.name));
245        }
246
247        // Compare node counts
248        if graph.node_count() != restored.node_count() {
249            diffs.push(format!("Node count mismatch: {} vs {}",
250                graph.node_count(), restored.node_count()));
251        }
252
253        // Compare connection counts
254        if graph.connections().len() != restored.connections().len() {
255            diffs.push(format!("Connection count mismatch: {} vs {}",
256                graph.connections().len(), restored.connections().len()));
257        }
258
259        // Compare individual nodes
260        for nid in graph.node_ids() {
261            let orig = graph.node(&nid);
262            let rest = restored.node(&nid);
263            match (orig, rest) {
264                (Some(o), Some(r)) => {
265                    if o.node_type != r.node_type {
266                        diffs.push(format!("Node {} type mismatch: {:?} vs {:?}",
267                            nid.0, o.node_type, r.node_type));
268                    }
269                    if o.label != r.label {
270                        diffs.push(format!("Node {} label mismatch: '{}' vs '{}'",
271                            nid.0, o.label, r.label));
272                    }
273                    if o.enabled != r.enabled {
274                        diffs.push(format!("Node {} enabled mismatch: {} vs {}",
275                            nid.0, o.enabled, r.enabled));
276                    }
277                    // Compare input defaults
278                    for (idx, (os, rs)) in o.inputs.iter().zip(r.inputs.iter()).enumerate() {
279                        if os.default_value != rs.default_value {
280                            diffs.push(format!("Node {} input {} default mismatch",
281                                nid.0, idx));
282                        }
283                    }
284                    // Compare properties
285                    if o.properties.len() != r.properties.len() {
286                        diffs.push(format!("Node {} property count mismatch: {} vs {}",
287                            nid.0, o.properties.len(), r.properties.len()));
288                    }
289                }
290                (Some(_), None) => {
291                    diffs.push(format!("Node {} missing in restored graph", nid.0));
292                }
293                (None, Some(_)) => {
294                    diffs.push(format!("Node {} unexpected in restored graph", nid.0));
295                }
296                (None, None) => {}
297            }
298        }
299
300        // Compare connections
301        let orig_conns: std::collections::HashSet<_> = graph.connections().iter()
302            .map(|c| (c.from_node.0, c.from_socket, c.to_node.0, c.to_socket))
303            .collect();
304        let rest_conns: std::collections::HashSet<_> = restored.connections().iter()
305            .map(|c| (c.from_node.0, c.from_socket, c.to_node.0, c.to_socket))
306            .collect();
307
308        for c in &orig_conns {
309            if !rest_conns.contains(c) {
310                diffs.push(format!("Connection {}:{} -> {}:{} missing in restored",
311                    c.0, c.1, c.2, c.3));
312            }
313        }
314        for c in &rest_conns {
315            if !orig_conns.contains(c) {
316                diffs.push(format!("Connection {}:{} -> {}:{} unexpected in restored",
317                    c.0, c.1, c.2, c.3));
318            }
319        }
320
321        Ok(diffs)
322    }
323}
324
325// ---------------------------------------------------------------------------
326// Parameter value serialization
327// ---------------------------------------------------------------------------
328
329/// Serialize a ParamValue to a type-tagged string: "type:value".
330fn serialize_param_value(val: &ParamValue) -> String {
331    match val {
332        ParamValue::Float(v) => format!("float:{}", format_f32(*v)),
333        ParamValue::Vec2(v) => format!("vec2:{},{}", format_f32(v[0]), format_f32(v[1])),
334        ParamValue::Vec3(v) => format!("vec3:{},{},{}", format_f32(v[0]), format_f32(v[1]), format_f32(v[2])),
335        ParamValue::Vec4(v) => format!("vec4:{},{},{},{}", format_f32(v[0]), format_f32(v[1]), format_f32(v[2]), format_f32(v[3])),
336        ParamValue::Int(v) => format!("int:{}", v),
337        ParamValue::Bool(v) => format!("bool:{}", v),
338        ParamValue::String(v) => format!("string:{}", v),
339    }
340}
341
342/// Deserialize a type-tagged string to a ParamValue.
343fn deserialize_param_value(s: &str) -> Result<ParamValue, SerializeError> {
344    let colon_pos = s.find(':')
345        .ok_or_else(|| SerializeError::ParseError(format!("No type tag in value: '{}'", s)))?;
346    let type_tag = &s[..colon_pos];
347    let value_str = &s[colon_pos + 1..];
348
349    match type_tag {
350        "float" => {
351            let v: f32 = value_str.parse()
352                .map_err(|e| SerializeError::ParseError(format!("float: {}", e)))?;
353            Ok(ParamValue::Float(v))
354        }
355        "vec2" => {
356            let parts: Vec<&str> = value_str.split(',').collect();
357            if parts.len() != 2 {
358                return Err(SerializeError::ParseError("vec2 needs 2 components".to_string()));
359            }
360            let x: f32 = parts[0].trim().parse().map_err(|e| SerializeError::ParseError(format!("vec2.x: {}", e)))?;
361            let y: f32 = parts[1].trim().parse().map_err(|e| SerializeError::ParseError(format!("vec2.y: {}", e)))?;
362            Ok(ParamValue::Vec2([x, y]))
363        }
364        "vec3" => {
365            let parts: Vec<&str> = value_str.split(',').collect();
366            if parts.len() != 3 {
367                return Err(SerializeError::ParseError("vec3 needs 3 components".to_string()));
368            }
369            let x: f32 = parts[0].trim().parse().map_err(|e| SerializeError::ParseError(format!("vec3.x: {}", e)))?;
370            let y: f32 = parts[1].trim().parse().map_err(|e| SerializeError::ParseError(format!("vec3.y: {}", e)))?;
371            let z: f32 = parts[2].trim().parse().map_err(|e| SerializeError::ParseError(format!("vec3.z: {}", e)))?;
372            Ok(ParamValue::Vec3([x, y, z]))
373        }
374        "vec4" => {
375            let parts: Vec<&str> = value_str.split(',').collect();
376            if parts.len() != 4 {
377                return Err(SerializeError::ParseError("vec4 needs 4 components".to_string()));
378            }
379            let x: f32 = parts[0].trim().parse().map_err(|e| SerializeError::ParseError(format!("vec4.x: {}", e)))?;
380            let y: f32 = parts[1].trim().parse().map_err(|e| SerializeError::ParseError(format!("vec4.y: {}", e)))?;
381            let z: f32 = parts[2].trim().parse().map_err(|e| SerializeError::ParseError(format!("vec4.z: {}", e)))?;
382            let w: f32 = parts[3].trim().parse().map_err(|e| SerializeError::ParseError(format!("vec4.w: {}", e)))?;
383            Ok(ParamValue::Vec4([x, y, z, w]))
384        }
385        "int" => {
386            let v: i32 = value_str.parse()
387                .map_err(|e| SerializeError::ParseError(format!("int: {}", e)))?;
388            Ok(ParamValue::Int(v))
389        }
390        "bool" => {
391            let v: bool = value_str.parse()
392                .map_err(|e| SerializeError::ParseError(format!("bool: {}", e)))?;
393            Ok(ParamValue::Bool(v))
394        }
395        "string" => {
396            Ok(ParamValue::String(value_str.to_string()))
397        }
398        _ => Err(SerializeError::ParseError(format!("Unknown type tag: '{}'", type_tag))),
399    }
400}
401
402// ---------------------------------------------------------------------------
403// Node type string mapping
404// ---------------------------------------------------------------------------
405
406fn node_type_to_string(nt: &NodeType) -> &'static str {
407    match nt {
408        NodeType::Color => "Color",
409        NodeType::Texture => "Texture",
410        NodeType::VertexPosition => "VertexPosition",
411        NodeType::VertexNormal => "VertexNormal",
412        NodeType::Time => "Time",
413        NodeType::CameraPos => "CameraPos",
414        NodeType::GameStateVar => "GameStateVar",
415        NodeType::Translate => "Translate",
416        NodeType::Rotate => "Rotate",
417        NodeType::Scale => "Scale",
418        NodeType::WorldToLocal => "WorldToLocal",
419        NodeType::LocalToWorld => "LocalToWorld",
420        NodeType::Add => "Add",
421        NodeType::Sub => "Sub",
422        NodeType::Mul => "Mul",
423        NodeType::Div => "Div",
424        NodeType::Dot => "Dot",
425        NodeType::Cross => "Cross",
426        NodeType::Normalize => "Normalize",
427        NodeType::Length => "Length",
428        NodeType::Abs => "Abs",
429        NodeType::Floor => "Floor",
430        NodeType::Ceil => "Ceil",
431        NodeType::Fract => "Fract",
432        NodeType::Mod => "Mod",
433        NodeType::Pow => "Pow",
434        NodeType::Sqrt => "Sqrt",
435        NodeType::Sin => "Sin",
436        NodeType::Cos => "Cos",
437        NodeType::Tan => "Tan",
438        NodeType::Atan2 => "Atan2",
439        NodeType::Lerp => "Lerp",
440        NodeType::Clamp => "Clamp",
441        NodeType::Smoothstep => "Smoothstep",
442        NodeType::Remap => "Remap",
443        NodeType::Step => "Step",
444        NodeType::Fresnel => "Fresnel",
445        NodeType::Dissolve => "Dissolve",
446        NodeType::Distortion => "Distortion",
447        NodeType::Blur => "Blur",
448        NodeType::Sharpen => "Sharpen",
449        NodeType::EdgeDetect => "EdgeDetect",
450        NodeType::Outline => "Outline",
451        NodeType::Bloom => "Bloom",
452        NodeType::ChromaticAberration => "ChromaticAberration",
453        NodeType::HSVToRGB => "HSVToRGB",
454        NodeType::RGBToHSV => "RGBToHSV",
455        NodeType::Contrast => "Contrast",
456        NodeType::Saturation => "Saturation",
457        NodeType::Hue => "Hue",
458        NodeType::Invert => "Invert",
459        NodeType::Posterize => "Posterize",
460        NodeType::GradientMap => "GradientMap",
461        NodeType::Perlin => "Perlin",
462        NodeType::Simplex => "Simplex",
463        NodeType::Voronoi => "Voronoi",
464        NodeType::FBM => "FBM",
465        NodeType::Turbulence => "Turbulence",
466        NodeType::MainColor => "MainColor",
467        NodeType::EmissionBuffer => "EmissionBuffer",
468        NodeType::BloomBuffer => "BloomBuffer",
469        NodeType::NormalOutput => "NormalOutput",
470    }
471}
472
473fn string_to_node_type(s: &str) -> Result<NodeType, SerializeError> {
474    match s {
475        "Color" => Ok(NodeType::Color),
476        "Texture" => Ok(NodeType::Texture),
477        "VertexPosition" => Ok(NodeType::VertexPosition),
478        "VertexNormal" => Ok(NodeType::VertexNormal),
479        "Time" => Ok(NodeType::Time),
480        "CameraPos" => Ok(NodeType::CameraPos),
481        "GameStateVar" => Ok(NodeType::GameStateVar),
482        "Translate" => Ok(NodeType::Translate),
483        "Rotate" => Ok(NodeType::Rotate),
484        "Scale" => Ok(NodeType::Scale),
485        "WorldToLocal" => Ok(NodeType::WorldToLocal),
486        "LocalToWorld" => Ok(NodeType::LocalToWorld),
487        "Add" => Ok(NodeType::Add),
488        "Sub" => Ok(NodeType::Sub),
489        "Mul" => Ok(NodeType::Mul),
490        "Div" => Ok(NodeType::Div),
491        "Dot" => Ok(NodeType::Dot),
492        "Cross" => Ok(NodeType::Cross),
493        "Normalize" => Ok(NodeType::Normalize),
494        "Length" => Ok(NodeType::Length),
495        "Abs" => Ok(NodeType::Abs),
496        "Floor" => Ok(NodeType::Floor),
497        "Ceil" => Ok(NodeType::Ceil),
498        "Fract" => Ok(NodeType::Fract),
499        "Mod" => Ok(NodeType::Mod),
500        "Pow" => Ok(NodeType::Pow),
501        "Sqrt" => Ok(NodeType::Sqrt),
502        "Sin" => Ok(NodeType::Sin),
503        "Cos" => Ok(NodeType::Cos),
504        "Tan" => Ok(NodeType::Tan),
505        "Atan2" => Ok(NodeType::Atan2),
506        "Lerp" => Ok(NodeType::Lerp),
507        "Clamp" => Ok(NodeType::Clamp),
508        "Smoothstep" => Ok(NodeType::Smoothstep),
509        "Remap" => Ok(NodeType::Remap),
510        "Step" => Ok(NodeType::Step),
511        "Fresnel" => Ok(NodeType::Fresnel),
512        "Dissolve" => Ok(NodeType::Dissolve),
513        "Distortion" => Ok(NodeType::Distortion),
514        "Blur" => Ok(NodeType::Blur),
515        "Sharpen" => Ok(NodeType::Sharpen),
516        "EdgeDetect" => Ok(NodeType::EdgeDetect),
517        "Outline" => Ok(NodeType::Outline),
518        "Bloom" => Ok(NodeType::Bloom),
519        "ChromaticAberration" => Ok(NodeType::ChromaticAberration),
520        "HSVToRGB" => Ok(NodeType::HSVToRGB),
521        "RGBToHSV" => Ok(NodeType::RGBToHSV),
522        "Contrast" => Ok(NodeType::Contrast),
523        "Saturation" => Ok(NodeType::Saturation),
524        "Hue" => Ok(NodeType::Hue),
525        "Invert" => Ok(NodeType::Invert),
526        "Posterize" => Ok(NodeType::Posterize),
527        "GradientMap" => Ok(NodeType::GradientMap),
528        "Perlin" => Ok(NodeType::Perlin),
529        "Simplex" => Ok(NodeType::Simplex),
530        "Voronoi" => Ok(NodeType::Voronoi),
531        "FBM" => Ok(NodeType::FBM),
532        "Turbulence" => Ok(NodeType::Turbulence),
533        "MainColor" => Ok(NodeType::MainColor),
534        "EmissionBuffer" => Ok(NodeType::EmissionBuffer),
535        "BloomBuffer" => Ok(NodeType::BloomBuffer),
536        "NormalOutput" => Ok(NodeType::NormalOutput),
537        _ => Err(SerializeError::UnknownNodeType(s.to_string())),
538    }
539}
540
541// ---------------------------------------------------------------------------
542// Parsing helpers
543// ---------------------------------------------------------------------------
544
545#[derive(Debug, Clone, Copy)]
546enum Section {
547    None,
548    Graph,
549    Node,
550    Connection,
551}
552
553/// Builder for constructing a ShaderNode during parsing.
554#[derive(Default)]
555struct NodeBuilder {
556    id: Option<u64>,
557    node_type: Option<String>,
558    label: Option<String>,
559    enabled: Option<bool>,
560    editor_x: Option<f32>,
561    editor_y: Option<f32>,
562    conditional_var: Option<String>,
563    conditional_threshold: Option<f32>,
564    input_defaults: HashMap<usize, ParamValue>,
565    properties: HashMap<String, ParamValue>,
566}
567
568/// Builder for constructing a Connection during parsing.
569#[derive(Default)]
570struct ConnBuilder {
571    from_node: Option<u64>,
572    from_socket: Option<usize>,
573    to_node: Option<u64>,
574    to_socket: Option<usize>,
575}
576
577/// Parse a "key = value" line.
578fn parse_kv(line: &str) -> Option<(String, String)> {
579    let eq_pos = line.find('=')?;
580    let key = line[..eq_pos].trim().to_string();
581    let value = line[eq_pos + 1..].trim().to_string();
582    Some((key, value))
583}
584
585/// Remove surrounding quotes from a string.
586fn unquote(s: &str) -> String {
587    let s = s.trim();
588    if s.len() >= 2 && s.starts_with('"') && s.ends_with('"') {
589        unescape_string(&s[1..s.len() - 1])
590    } else {
591        s.to_string()
592    }
593}
594
595/// Escape special characters for serialization.
596fn escape_string(s: &str) -> String {
597    s.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n")
598}
599
600/// Unescape special characters during deserialization.
601fn unescape_string(s: &str) -> String {
602    let mut result = String::new();
603    let mut chars = s.chars();
604    while let Some(c) = chars.next() {
605        if c == '\\' {
606            match chars.next() {
607                Some('n') => result.push('\n'),
608                Some('"') => result.push('"'),
609                Some('\\') => result.push('\\'),
610                Some(other) => { result.push('\\'); result.push(other); }
611                None => result.push('\\'),
612            }
613        } else {
614            result.push(c);
615        }
616    }
617    result
618}
619
620/// Format a float for serialization (enough precision for round-trip).
621fn format_f32(v: f32) -> String {
622    if v == 0.0 {
623        "0".to_string()
624    } else if v == v.floor() && v.abs() < 1e7 {
625        format!("{}", v as i64)
626    } else {
627        format!("{:.6}", v)
628    }
629}
630
631fn parse_node_field(nb: &mut NodeBuilder, key: &str, value: &str) -> Result<(), SerializeError> {
632    match key {
633        "id" => {
634            nb.id = Some(value.parse().map_err(|e| {
635                SerializeError::ParseError(format!("node id: {}", e))
636            })?);
637        }
638        "type" => {
639            nb.node_type = Some(unquote(value));
640        }
641        "label" => {
642            nb.label = Some(unquote(value));
643        }
644        "enabled" => {
645            nb.enabled = Some(value.parse().map_err(|e| {
646                SerializeError::ParseError(format!("enabled: {}", e))
647            })?);
648        }
649        "editor_x" => {
650            nb.editor_x = Some(value.parse().map_err(|e| {
651                SerializeError::ParseError(format!("editor_x: {}", e))
652            })?);
653        }
654        "editor_y" => {
655            nb.editor_y = Some(value.parse().map_err(|e| {
656                SerializeError::ParseError(format!("editor_y: {}", e))
657            })?);
658        }
659        "conditional_var" => {
660            nb.conditional_var = Some(unquote(value));
661        }
662        "conditional_threshold" => {
663            nb.conditional_threshold = Some(value.parse().map_err(|e| {
664                SerializeError::ParseError(format!("conditional_threshold: {}", e))
665            })?);
666        }
667        _ if key.starts_with("input.") => {
668            // Parse "input.N.default"
669            let rest = &key["input.".len()..];
670            if let Some(dot_pos) = rest.find('.') {
671                let idx: usize = rest[..dot_pos].parse().map_err(|e| {
672                    SerializeError::ParseError(format!("input index: {}", e))
673                })?;
674                let field = &rest[dot_pos + 1..];
675                if field == "default" {
676                    let val_str = unquote(value);
677                    let pv = deserialize_param_value(&val_str)?;
678                    nb.input_defaults.insert(idx, pv);
679                }
680            }
681        }
682        _ if key.starts_with("property.") => {
683            let prop_name = key["property.".len()..].to_string();
684            let val_str = unquote(value);
685            let pv = deserialize_param_value(&val_str)?;
686            nb.properties.insert(prop_name, pv);
687        }
688        _ => {} // ignore unknown
689    }
690    Ok(())
691}
692
693fn parse_conn_field(cb: &mut ConnBuilder, key: &str, value: &str) -> Result<(), SerializeError> {
694    match key {
695        "from" => {
696            let (node, socket) = parse_node_socket(value)?;
697            cb.from_node = Some(node);
698            cb.from_socket = Some(socket);
699        }
700        "to" => {
701            let (node, socket) = parse_node_socket(value)?;
702            cb.to_node = Some(node);
703            cb.to_socket = Some(socket);
704        }
705        _ => {}
706    }
707    Ok(())
708}
709
710/// Parse "node_id:socket_idx" format.
711fn parse_node_socket(s: &str) -> Result<(u64, usize), SerializeError> {
712    let parts: Vec<&str> = s.trim().split(':').collect();
713    if parts.len() != 2 {
714        return Err(SerializeError::ParseError(format!("Expected node:socket format, got '{}'", s)));
715    }
716    let node: u64 = parts[0].parse().map_err(|e| {
717        SerializeError::ParseError(format!("node id in connection: {}", e))
718    })?;
719    let socket: usize = parts[1].parse().map_err(|e| {
720        SerializeError::ParseError(format!("socket index in connection: {}", e))
721    })?;
722    Ok((node, socket))
723}
724
725/// Flush the current node builder into the nodes list.
726fn flush_node(current: &mut Option<NodeBuilder>, nodes: &mut Vec<ShaderNode>) -> Result<(), SerializeError> {
727    if let Some(nb) = current.take() {
728        let id = nb.id.ok_or_else(|| SerializeError::MissingField("node id".to_string()))?;
729        let type_str = nb.node_type.ok_or_else(|| SerializeError::MissingField("node type".to_string()))?;
730        let node_type = string_to_node_type(&type_str)?;
731
732        let mut node = ShaderNode::new(NodeId(id), node_type);
733        if let Some(label) = nb.label {
734            node.label = label;
735        }
736        if let Some(enabled) = nb.enabled {
737            node.enabled = enabled;
738        }
739        if let Some(x) = nb.editor_x {
740            node.editor_x = x;
741        }
742        if let Some(y) = nb.editor_y {
743            node.editor_y = y;
744        }
745        node.conditional_var = nb.conditional_var;
746        node.conditional_threshold = nb.conditional_threshold.unwrap_or(0.0);
747
748        // Apply input defaults
749        for (idx, val) in nb.input_defaults {
750            if idx < node.inputs.len() {
751                node.inputs[idx].default_value = Some(val);
752            }
753        }
754
755        // Apply properties
756        node.properties = nb.properties;
757
758        nodes.push(node);
759    }
760    Ok(())
761}
762
763/// Flush the current connection builder into the connections list.
764fn flush_conn(current: &mut Option<ConnBuilder>, conns: &mut Vec<Connection>) -> Result<(), SerializeError> {
765    if let Some(cb) = current.take() {
766        let from_node = cb.from_node.ok_or_else(|| SerializeError::MissingField("connection from".to_string()))?;
767        let from_socket = cb.from_socket.ok_or_else(|| SerializeError::MissingField("connection from socket".to_string()))?;
768        let to_node = cb.to_node.ok_or_else(|| SerializeError::MissingField("connection to".to_string()))?;
769        let to_socket = cb.to_socket.ok_or_else(|| SerializeError::MissingField("connection to socket".to_string()))?;
770
771        conns.push(Connection::new(
772            NodeId(from_node), from_socket,
773            NodeId(to_node), to_socket,
774        ));
775    }
776    Ok(())
777}