Skip to main content

alice_sdf/
diff.rs

1//! CSG Tree structural diff and patch
2//!
3//! Computes a minimal set of operations to transform one SDF tree into another.
4//! Useful for undo/redo systems, network synchronization, and collaborative editing.
5//!
6//! Author: Moroya Sakamoto
7
8use crate::crispy::fnv1a_hash;
9use crate::types::SdfNode;
10use std::sync::Arc;
11
12// ── Types ────────────────────────────────────────────────────
13
14/// A path from the root to a specific node in the SDF tree.
15///
16/// Each element is the child index at that depth level.
17/// Example: `[0, 2, 1]` means root → 0th child → 2nd child → 1st child.
18pub type TreePath = Vec<usize>;
19
20/// A single diff operation.
21#[derive(Debug, Clone)]
22pub enum DiffOp {
23    /// Replace the node at `path` with a new node.
24    Replace {
25        /// Path to the node to replace.
26        path: TreePath,
27        /// Hash of the old node (for verification).
28        old_hash: u64,
29        /// Replacement node.
30        new_node: Arc<SdfNode>,
31    },
32    /// Insert a wrapper node at `path` (wraps existing node as child).
33    Insert {
34        /// Path to the node to wrap.
35        path: TreePath,
36        /// Hash of the existing node (for verification).
37        old_hash: u64,
38        /// Wrapper constructor: the existing node becomes the child.
39        wrapper: Arc<SdfNode>,
40    },
41    /// Delete a wrapper node at `path`, replacing it with its child.
42    Delete {
43        /// Path to the wrapper node to unwrap.
44        path: TreePath,
45        /// Hash of the wrapper node (for verification).
46        old_hash: u64,
47        /// Index of the child to promote (0 for single-child nodes).
48        child_index: usize,
49    },
50}
51
52/// Error type for patch application.
53#[derive(Debug, Clone)]
54pub enum DiffError {
55    /// The path does not lead to a valid node.
56    InvalidPath(TreePath),
57    /// Hash mismatch: the tree has changed since the diff was computed.
58    HashMismatch {
59        /// Path where the mismatch occurred.
60        path: TreePath,
61        /// Expected hash.
62        expected: u64,
63        /// Actual hash.
64        actual: u64,
65    },
66}
67
68impl std::fmt::Display for DiffError {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        match self {
71            Self::InvalidPath(p) => write!(f, "Invalid path: {:?}", p),
72            Self::HashMismatch {
73                path,
74                expected,
75                actual,
76            } => write!(
77                f,
78                "Hash mismatch at {:?}: expected {:#x}, got {:#x}",
79                path, expected, actual
80            ),
81        }
82    }
83}
84
85/// A patch containing a set of diff operations.
86#[derive(Debug, Clone)]
87pub struct TreePatch {
88    /// Ordered list of diff operations.
89    pub ops: Vec<DiffOp>,
90    /// Content hash of this patch (for deduplication).
91    pub content_hash: u64,
92}
93
94// ── Tree Hashing ─────────────────────────────────────────────
95
96/// Compute a deterministic structural hash of an SDF node.
97///
98/// Two structurally identical trees produce the same hash.
99/// Uses FNV-1a over a serialized representation.
100pub fn tree_hash(node: &SdfNode) -> u64 {
101    let serialized = format!("{:?}", node);
102    fnv1a_hash(serialized.as_bytes())
103}
104
105// ── Diff Computation ─────────────────────────────────────────
106
107/// Compute the structural diff between two SDF trees.
108///
109/// Returns a `TreePatch` describing the minimal set of `Replace` operations
110/// needed to transform `old` into `new`.
111pub fn tree_diff(old: &SdfNode, new: &SdfNode) -> TreePatch {
112    let mut ops = Vec::new();
113    diff_recursive(old, new, &mut vec![], &mut ops);
114
115    let mut hash_data = Vec::with_capacity(ops.len() * 8);
116    for (i, op) in ops.iter().enumerate() {
117        hash_data.extend_from_slice(&(i as u64).to_le_bytes());
118        match op {
119            DiffOp::Replace { old_hash, .. }
120            | DiffOp::Insert { old_hash, .. }
121            | DiffOp::Delete { old_hash, .. } => {
122                hash_data.extend_from_slice(&old_hash.to_le_bytes());
123            }
124        }
125    }
126    let content_hash = if hash_data.is_empty() {
127        0
128    } else {
129        fnv1a_hash(&hash_data)
130    };
131
132    TreePatch { ops, content_hash }
133}
134
135/// Recursive diff: compare old and new subtrees at the current path.
136fn diff_recursive(old: &SdfNode, new: &SdfNode, path: &mut Vec<usize>, ops: &mut Vec<DiffOp>) {
137    let old_hash = tree_hash(old);
138    let new_hash = tree_hash(new);
139
140    // Identical subtrees: skip
141    if old_hash == new_hash {
142        return;
143    }
144
145    // Try to recurse into matching structure
146    let old_children = get_children(old);
147    let new_children = get_children(new);
148
149    if same_variant(old, new)
150        && old_children.len() == new_children.len()
151        && !old_children.is_empty()
152    {
153        // Same node type with same child count. The node's own parameters
154        // (offset, k, factor, ...) are unchanged exactly when `old` with
155        // `new`'s children is `new`; only then can the change be expressed
156        // as child ops. Otherwise the child ops would drop the parameter
157        // change (until 4.1.0 a `Translate` whose offset and child both
158        // changed was patched to the new child at the old offset).
159        let mut grafted = old.clone();
160        for (i, child) in new_children.iter().enumerate() {
161            grafted = replace_child(&grafted, i, Arc::new((*child).clone()));
162        }
163
164        if tree_hash(&grafted) == new_hash {
165            for i in 0..old_children.len() {
166                path.push(i);
167                diff_recursive(old_children[i], new_children[i], path, ops);
168                path.pop();
169            }
170        } else {
171            ops.push(DiffOp::Replace {
172                path: path.clone(),
173                old_hash,
174                new_node: Arc::new(new.clone()),
175            });
176        }
177    } else {
178        // Different structure: emit Replace
179        ops.push(DiffOp::Replace {
180            path: path.clone(),
181            old_hash,
182            new_node: Arc::new(new.clone()),
183        });
184    }
185}
186
187/// Check if two SdfNode variants have the same discriminant.
188fn same_variant(a: &SdfNode, b: &SdfNode) -> bool {
189    std::mem::discriminant(a) == std::mem::discriminant(b)
190}
191
192/// Extract children of an SdfNode.
193fn get_children(node: &SdfNode) -> Vec<&SdfNode> {
194    match node {
195        // Operations with two children
196        SdfNode::Union { a, b }
197        | SdfNode::Intersection { a, b }
198        | SdfNode::Subtraction { a, b }
199        | SdfNode::SmoothUnion { a, b, .. }
200        | SdfNode::SmoothIntersection { a, b, .. }
201        | SdfNode::SmoothSubtraction { a, b, .. }
202        | SdfNode::ChamferUnion { a, b, .. }
203        | SdfNode::ChamferIntersection { a, b, .. }
204        | SdfNode::ChamferSubtraction { a, b, .. }
205        | SdfNode::StairsUnion { a, b, .. }
206        | SdfNode::StairsIntersection { a, b, .. }
207        | SdfNode::StairsSubtraction { a, b, .. }
208        | SdfNode::XOR { a, b }
209        | SdfNode::Morph { a, b, .. }
210        | SdfNode::ColumnsUnion { a, b, .. }
211        | SdfNode::ColumnsIntersection { a, b, .. }
212        | SdfNode::ColumnsSubtraction { a, b, .. }
213        | SdfNode::Pipe { a, b, .. }
214        | SdfNode::Engrave { a, b, .. }
215        | SdfNode::Groove { a, b, .. }
216        | SdfNode::Tongue { a, b, .. }
217        | SdfNode::ExpSmoothUnion { a, b, .. }
218        | SdfNode::ExpSmoothIntersection { a, b, .. }
219        | SdfNode::ExpSmoothSubtraction { a, b, .. } => vec![a.as_ref(), b.as_ref()],
220
221        // Single-child transforms and modifiers
222        SdfNode::Translate { child, .. }
223        | SdfNode::Rotate { child, .. }
224        | SdfNode::Scale { child, .. }
225        | SdfNode::ScaleNonUniform { child, .. }
226        | SdfNode::ProjectiveTransform { child, .. }
227        | SdfNode::LatticeDeform { child, .. }
228        | SdfNode::SdfSkinning { child, .. }
229        | SdfNode::Twist { child, .. }
230        | SdfNode::Bend { child, .. }
231        | SdfNode::RepeatInfinite { child, .. }
232        | SdfNode::RepeatFinite { child, .. }
233        | SdfNode::Noise { child, .. }
234        | SdfNode::Round { child, .. }
235        | SdfNode::Onion { child, .. }
236        | SdfNode::Elongate { child, .. }
237        | SdfNode::Mirror { child, .. }
238        | SdfNode::OctantMirror { child }
239        | SdfNode::Revolution { child, .. }
240        | SdfNode::Extrude { child, .. }
241        | SdfNode::Taper { child, .. }
242        | SdfNode::Displacement { child, .. }
243        | SdfNode::SineDisplacement { child, .. }
244        | SdfNode::PolarRepeat { child, .. }
245        | SdfNode::SweepBezier { child, .. }
246        | SdfNode::IcosahedralSymmetry { child }
247        | SdfNode::IFS { child, .. }
248        | SdfNode::HeightmapDisplacement { child, .. }
249        | SdfNode::SurfaceRoughness { child, .. }
250        | SdfNode::Shear { child, .. }
251        | SdfNode::Animated { child, .. }
252        | SdfNode::WithMaterial { child, .. } => vec![child.as_ref()],
253
254        // Leaf nodes: no children
255        _ => vec![],
256    }
257}
258
259// ── Patch Application ────────────────────────────────────────
260
261/// Apply a patch to an SDF tree, producing a new tree.
262pub fn apply_patch(tree: &SdfNode, patch: &TreePatch) -> Result<SdfNode, DiffError> {
263    let mut result = tree.clone();
264    for op in &patch.ops {
265        // The path helpers see only the remaining suffix, so a hash mismatch
266        // comes back with an empty path; report the op's full path instead.
267        let at = |e: DiffError, op_path: &TreePath| match e {
268            DiffError::HashMismatch {
269                expected, actual, ..
270            } => DiffError::HashMismatch {
271                path: op_path.clone(),
272                expected,
273                actual,
274            },
275            other => other,
276        };
277        match op {
278            DiffOp::Replace {
279                path,
280                old_hash,
281                new_node,
282            } => {
283                result =
284                    replace_at_path(&result, path, *old_hash, new_node).map_err(|e| at(e, path))?;
285            }
286            DiffOp::Insert {
287                path,
288                old_hash,
289                wrapper,
290            } => {
291                result =
292                    insert_at_path(&result, path, *old_hash, wrapper).map_err(|e| at(e, path))?;
293            }
294            DiffOp::Delete {
295                path,
296                old_hash,
297                child_index,
298            } => {
299                result = delete_at_path(&result, path, *old_hash, *child_index)
300                    .map_err(|e| at(e, path))?;
301            }
302        }
303    }
304    Ok(result)
305}
306
307/// Invert a patch: creates an undo patch from a forward patch.
308///
309/// The inverted patch, when applied to the result tree, produces the original tree.
310/// Invert a patch: produces a patch that undoes the given patch.
311/// Works for all operation types (Replace, Insert, Delete) by
312/// applying the patch then computing the reverse diff.
313pub fn invert_patch(original: &SdfNode, patch: &TreePatch) -> Result<TreePatch, DiffError> {
314    let result = apply_patch(original, patch)?;
315    Ok(tree_diff(&result, original))
316}
317
318/// Merge two sequential patches into a single patch.
319///
320/// `patch_a` is applied first, then `patch_b`. The merged patch
321/// transforms the original tree directly to the final state.
322pub fn merge_patches(
323    original: &SdfNode,
324    patch_a: &TreePatch,
325    patch_b: &TreePatch,
326) -> Result<TreePatch, DiffError> {
327    let after_a = apply_patch(original, patch_a)?;
328    let after_b = apply_patch(&after_a, patch_b)?;
329    Ok(tree_diff(original, &after_b))
330}
331
332/// Count the number of operations in a patch.
333impl TreePatch {
334    /// Number of operations.
335    pub fn op_count(&self) -> usize {
336        self.ops.len()
337    }
338
339    /// Whether this patch is a no-op (identity).
340    pub fn is_empty(&self) -> bool {
341        self.ops.is_empty()
342    }
343}
344
345/// Replace the node at the given path.
346fn replace_at_path(
347    node: &SdfNode,
348    path: &[usize],
349    expected_hash: u64,
350    replacement: &Arc<SdfNode>,
351) -> Result<SdfNode, DiffError> {
352    if path.is_empty() {
353        let actual_hash = tree_hash(node);
354        if actual_hash != expected_hash {
355            return Err(DiffError::HashMismatch {
356                path: vec![],
357                expected: expected_hash,
358                actual: actual_hash,
359            });
360        }
361        return Ok(replacement.as_ref().clone());
362    }
363
364    let idx = path[0];
365    let rest = &path[1..];
366    let children = get_children(node);
367
368    if idx >= children.len() {
369        return Err(DiffError::InvalidPath(path.to_vec()));
370    }
371
372    let new_child = Arc::new(replace_at_path(
373        children[idx],
374        rest,
375        expected_hash,
376        replacement,
377    )?);
378    Ok(replace_child(node, idx, new_child))
379}
380
381/// Insert a wrapper node at the given path (wraps the existing node).
382fn insert_at_path(
383    node: &SdfNode,
384    path: &[usize],
385    expected_hash: u64,
386    wrapper: &Arc<SdfNode>,
387) -> Result<SdfNode, DiffError> {
388    if path.is_empty() {
389        let actual_hash = tree_hash(node);
390        if actual_hash != expected_hash {
391            return Err(DiffError::HashMismatch {
392                path: vec![],
393                expected: expected_hash,
394                actual: actual_hash,
395            });
396        }
397        // The wrapper's first single child is replaced with the existing node
398        let children = get_children(wrapper);
399        if children.len() == 1 {
400            return Ok(replace_child(wrapper, 0, Arc::new(node.clone())));
401        }
402        // For binary ops, existing node goes into child 0
403        Ok(replace_child(wrapper, 0, Arc::new(node.clone())))
404    } else {
405        let idx = path[0];
406        let rest = &path[1..];
407        let children = get_children(node);
408        if idx >= children.len() {
409            return Err(DiffError::InvalidPath(path.to_vec()));
410        }
411        let new_child = Arc::new(insert_at_path(children[idx], rest, expected_hash, wrapper)?);
412        Ok(replace_child(node, idx, new_child))
413    }
414}
415
416/// Delete a wrapper node at the given path, promoting its child.
417fn delete_at_path(
418    node: &SdfNode,
419    path: &[usize],
420    expected_hash: u64,
421    child_index: usize,
422) -> Result<SdfNode, DiffError> {
423    if path.is_empty() {
424        let actual_hash = tree_hash(node);
425        if actual_hash != expected_hash {
426            return Err(DiffError::HashMismatch {
427                path: vec![],
428                expected: expected_hash,
429                actual: actual_hash,
430            });
431        }
432        let children = get_children(node);
433        if child_index >= children.len() {
434            return Err(DiffError::InvalidPath(vec![child_index]));
435        }
436        Ok(children[child_index].clone())
437    } else {
438        let idx = path[0];
439        let rest = &path[1..];
440        let children = get_children(node);
441        if idx >= children.len() {
442            return Err(DiffError::InvalidPath(path.to_vec()));
443        }
444        let new_child = Arc::new(delete_at_path(
445            children[idx],
446            rest,
447            expected_hash,
448            child_index,
449        )?);
450        Ok(replace_child(node, idx, new_child))
451    }
452}
453
454/// Create a clone of the node with one child replaced.
455fn replace_child(node: &SdfNode, child_idx: usize, new_child: Arc<SdfNode>) -> SdfNode {
456    match node {
457        // ── Two-child operations ──────────────────────────────────
458        SdfNode::Union { a, b } => {
459            if child_idx == 0 {
460                SdfNode::Union {
461                    a: new_child,
462                    b: b.clone(),
463                }
464            } else {
465                SdfNode::Union {
466                    a: a.clone(),
467                    b: new_child,
468                }
469            }
470        }
471        SdfNode::Intersection { a, b } => {
472            if child_idx == 0 {
473                SdfNode::Intersection {
474                    a: new_child,
475                    b: b.clone(),
476                }
477            } else {
478                SdfNode::Intersection {
479                    a: a.clone(),
480                    b: new_child,
481                }
482            }
483        }
484        SdfNode::Subtraction { a, b } => {
485            if child_idx == 0 {
486                SdfNode::Subtraction {
487                    a: new_child,
488                    b: b.clone(),
489                }
490            } else {
491                SdfNode::Subtraction {
492                    a: a.clone(),
493                    b: new_child,
494                }
495            }
496        }
497        SdfNode::SmoothUnion { a, b, k } => {
498            if child_idx == 0 {
499                SdfNode::SmoothUnion {
500                    a: new_child,
501                    b: b.clone(),
502                    k: *k,
503                }
504            } else {
505                SdfNode::SmoothUnion {
506                    a: a.clone(),
507                    b: new_child,
508                    k: *k,
509                }
510            }
511        }
512        SdfNode::SmoothIntersection { a, b, k } => {
513            if child_idx == 0 {
514                SdfNode::SmoothIntersection {
515                    a: new_child,
516                    b: b.clone(),
517                    k: *k,
518                }
519            } else {
520                SdfNode::SmoothIntersection {
521                    a: a.clone(),
522                    b: new_child,
523                    k: *k,
524                }
525            }
526        }
527        SdfNode::SmoothSubtraction { a, b, k } => {
528            if child_idx == 0 {
529                SdfNode::SmoothSubtraction {
530                    a: new_child,
531                    b: b.clone(),
532                    k: *k,
533                }
534            } else {
535                SdfNode::SmoothSubtraction {
536                    a: a.clone(),
537                    b: new_child,
538                    k: *k,
539                }
540            }
541        }
542        SdfNode::ChamferUnion { a, b, r } => {
543            if child_idx == 0 {
544                SdfNode::ChamferUnion {
545                    a: new_child,
546                    b: b.clone(),
547                    r: *r,
548                }
549            } else {
550                SdfNode::ChamferUnion {
551                    a: a.clone(),
552                    b: new_child,
553                    r: *r,
554                }
555            }
556        }
557        SdfNode::ChamferIntersection { a, b, r } => {
558            if child_idx == 0 {
559                SdfNode::ChamferIntersection {
560                    a: new_child,
561                    b: b.clone(),
562                    r: *r,
563                }
564            } else {
565                SdfNode::ChamferIntersection {
566                    a: a.clone(),
567                    b: new_child,
568                    r: *r,
569                }
570            }
571        }
572        SdfNode::ChamferSubtraction { a, b, r } => {
573            if child_idx == 0 {
574                SdfNode::ChamferSubtraction {
575                    a: new_child,
576                    b: b.clone(),
577                    r: *r,
578                }
579            } else {
580                SdfNode::ChamferSubtraction {
581                    a: a.clone(),
582                    b: new_child,
583                    r: *r,
584                }
585            }
586        }
587        SdfNode::StairsUnion { a, b, r, n } => {
588            if child_idx == 0 {
589                SdfNode::StairsUnion {
590                    a: new_child,
591                    b: b.clone(),
592                    r: *r,
593                    n: *n,
594                }
595            } else {
596                SdfNode::StairsUnion {
597                    a: a.clone(),
598                    b: new_child,
599                    r: *r,
600                    n: *n,
601                }
602            }
603        }
604        SdfNode::StairsIntersection { a, b, r, n } => {
605            if child_idx == 0 {
606                SdfNode::StairsIntersection {
607                    a: new_child,
608                    b: b.clone(),
609                    r: *r,
610                    n: *n,
611                }
612            } else {
613                SdfNode::StairsIntersection {
614                    a: a.clone(),
615                    b: new_child,
616                    r: *r,
617                    n: *n,
618                }
619            }
620        }
621        SdfNode::StairsSubtraction { a, b, r, n } => {
622            if child_idx == 0 {
623                SdfNode::StairsSubtraction {
624                    a: new_child,
625                    b: b.clone(),
626                    r: *r,
627                    n: *n,
628                }
629            } else {
630                SdfNode::StairsSubtraction {
631                    a: a.clone(),
632                    b: new_child,
633                    r: *r,
634                    n: *n,
635                }
636            }
637        }
638        SdfNode::XOR { a, b } => {
639            if child_idx == 0 {
640                SdfNode::XOR {
641                    a: new_child,
642                    b: b.clone(),
643                }
644            } else {
645                SdfNode::XOR {
646                    a: a.clone(),
647                    b: new_child,
648                }
649            }
650        }
651        SdfNode::Morph { a, b, t } => {
652            if child_idx == 0 {
653                SdfNode::Morph {
654                    a: new_child,
655                    b: b.clone(),
656                    t: *t,
657                }
658            } else {
659                SdfNode::Morph {
660                    a: a.clone(),
661                    b: new_child,
662                    t: *t,
663                }
664            }
665        }
666        SdfNode::ColumnsUnion { a, b, r, n } => {
667            if child_idx == 0 {
668                SdfNode::ColumnsUnion {
669                    a: new_child,
670                    b: b.clone(),
671                    r: *r,
672                    n: *n,
673                }
674            } else {
675                SdfNode::ColumnsUnion {
676                    a: a.clone(),
677                    b: new_child,
678                    r: *r,
679                    n: *n,
680                }
681            }
682        }
683        SdfNode::ColumnsIntersection { a, b, r, n } => {
684            if child_idx == 0 {
685                SdfNode::ColumnsIntersection {
686                    a: new_child,
687                    b: b.clone(),
688                    r: *r,
689                    n: *n,
690                }
691            } else {
692                SdfNode::ColumnsIntersection {
693                    a: a.clone(),
694                    b: new_child,
695                    r: *r,
696                    n: *n,
697                }
698            }
699        }
700        SdfNode::ColumnsSubtraction { a, b, r, n } => {
701            if child_idx == 0 {
702                SdfNode::ColumnsSubtraction {
703                    a: new_child,
704                    b: b.clone(),
705                    r: *r,
706                    n: *n,
707                }
708            } else {
709                SdfNode::ColumnsSubtraction {
710                    a: a.clone(),
711                    b: new_child,
712                    r: *r,
713                    n: *n,
714                }
715            }
716        }
717        SdfNode::Pipe { a, b, r } => {
718            if child_idx == 0 {
719                SdfNode::Pipe {
720                    a: new_child,
721                    b: b.clone(),
722                    r: *r,
723                }
724            } else {
725                SdfNode::Pipe {
726                    a: a.clone(),
727                    b: new_child,
728                    r: *r,
729                }
730            }
731        }
732        SdfNode::Engrave { a, b, r } => {
733            if child_idx == 0 {
734                SdfNode::Engrave {
735                    a: new_child,
736                    b: b.clone(),
737                    r: *r,
738                }
739            } else {
740                SdfNode::Engrave {
741                    a: a.clone(),
742                    b: new_child,
743                    r: *r,
744                }
745            }
746        }
747        SdfNode::Groove { a, b, ra, rb } => {
748            if child_idx == 0 {
749                SdfNode::Groove {
750                    a: new_child,
751                    b: b.clone(),
752                    ra: *ra,
753                    rb: *rb,
754                }
755            } else {
756                SdfNode::Groove {
757                    a: a.clone(),
758                    b: new_child,
759                    ra: *ra,
760                    rb: *rb,
761                }
762            }
763        }
764        SdfNode::Tongue { a, b, ra, rb } => {
765            if child_idx == 0 {
766                SdfNode::Tongue {
767                    a: new_child,
768                    b: b.clone(),
769                    ra: *ra,
770                    rb: *rb,
771                }
772            } else {
773                SdfNode::Tongue {
774                    a: a.clone(),
775                    b: new_child,
776                    ra: *ra,
777                    rb: *rb,
778                }
779            }
780        }
781        SdfNode::ExpSmoothUnion { a, b, k } => {
782            if child_idx == 0 {
783                SdfNode::ExpSmoothUnion {
784                    a: new_child,
785                    b: b.clone(),
786                    k: *k,
787                }
788            } else {
789                SdfNode::ExpSmoothUnion {
790                    a: a.clone(),
791                    b: new_child,
792                    k: *k,
793                }
794            }
795        }
796        SdfNode::ExpSmoothIntersection { a, b, k } => {
797            if child_idx == 0 {
798                SdfNode::ExpSmoothIntersection {
799                    a: new_child,
800                    b: b.clone(),
801                    k: *k,
802                }
803            } else {
804                SdfNode::ExpSmoothIntersection {
805                    a: a.clone(),
806                    b: new_child,
807                    k: *k,
808                }
809            }
810        }
811        SdfNode::ExpSmoothSubtraction { a, b, k } => {
812            if child_idx == 0 {
813                SdfNode::ExpSmoothSubtraction {
814                    a: new_child,
815                    b: b.clone(),
816                    k: *k,
817                }
818            } else {
819                SdfNode::ExpSmoothSubtraction {
820                    a: a.clone(),
821                    b: new_child,
822                    k: *k,
823                }
824            }
825        }
826
827        // ── Single-child transforms ───────────────────────────────
828        SdfNode::Translate { offset, .. } => SdfNode::Translate {
829            child: new_child,
830            offset: *offset,
831        },
832        SdfNode::Rotate { rotation, .. } => SdfNode::Rotate {
833            child: new_child,
834            rotation: *rotation,
835        },
836        SdfNode::Scale { factor, .. } => SdfNode::Scale {
837            child: new_child,
838            factor: *factor,
839        },
840        SdfNode::ScaleNonUniform { factors, .. } => SdfNode::ScaleNonUniform {
841            child: new_child,
842            factors: *factors,
843        },
844        SdfNode::ProjectiveTransform {
845            inv_matrix,
846            lipschitz_bound,
847            ..
848        } => SdfNode::ProjectiveTransform {
849            child: new_child,
850            inv_matrix: *inv_matrix,
851            lipschitz_bound: *lipschitz_bound,
852        },
853        SdfNode::LatticeDeform {
854            control_points,
855            nx,
856            ny,
857            nz,
858            bbox_min,
859            bbox_max,
860            ..
861        } => SdfNode::LatticeDeform {
862            child: new_child,
863            control_points: control_points.clone(),
864            nx: *nx,
865            ny: *ny,
866            nz: *nz,
867            bbox_min: *bbox_min,
868            bbox_max: *bbox_max,
869        },
870        SdfNode::SdfSkinning { bones, .. } => SdfNode::SdfSkinning {
871            child: new_child,
872            bones: bones.clone(),
873        },
874
875        // ── Single-child modifiers ────────────────────────────────
876        SdfNode::Twist { strength, .. } => SdfNode::Twist {
877            child: new_child,
878            strength: *strength,
879        },
880        SdfNode::Bend { curvature, .. } => SdfNode::Bend {
881            child: new_child,
882            curvature: *curvature,
883        },
884        SdfNode::RepeatInfinite { spacing, .. } => SdfNode::RepeatInfinite {
885            child: new_child,
886            spacing: *spacing,
887        },
888        SdfNode::RepeatFinite { count, spacing, .. } => SdfNode::RepeatFinite {
889            child: new_child,
890            count: *count,
891            spacing: *spacing,
892        },
893        SdfNode::Noise {
894            amplitude,
895            frequency,
896            seed,
897            ..
898        } => SdfNode::Noise {
899            child: new_child,
900            amplitude: *amplitude,
901            frequency: *frequency,
902            seed: *seed,
903        },
904        SdfNode::Round { radius, .. } => SdfNode::Round {
905            child: new_child,
906            radius: *radius,
907        },
908        SdfNode::Onion { thickness, .. } => SdfNode::Onion {
909            child: new_child,
910            thickness: *thickness,
911        },
912        SdfNode::Elongate { amount, .. } => SdfNode::Elongate {
913            child: new_child,
914            amount: *amount,
915        },
916        SdfNode::Mirror { axes, .. } => SdfNode::Mirror {
917            child: new_child,
918            axes: *axes,
919        },
920        SdfNode::OctantMirror { .. } => SdfNode::OctantMirror { child: new_child },
921        SdfNode::Revolution { offset, .. } => SdfNode::Revolution {
922            child: new_child,
923            offset: *offset,
924        },
925        SdfNode::Extrude { half_height, .. } => SdfNode::Extrude {
926            child: new_child,
927            half_height: *half_height,
928        },
929        SdfNode::Taper { factor, reach, .. } => SdfNode::Taper {
930            child: new_child,
931            factor: *factor,
932            reach: *reach,
933        },
934        SdfNode::Displacement { strength, .. } => SdfNode::Displacement {
935            child: new_child,
936            strength: *strength,
937        },
938        SdfNode::SineDisplacement {
939            amplitude,
940            frequency,
941            ..
942        } => SdfNode::SineDisplacement {
943            child: new_child,
944            amplitude: *amplitude,
945            frequency: *frequency,
946        },
947        SdfNode::PolarRepeat { count, .. } => SdfNode::PolarRepeat {
948            child: new_child,
949            count: *count,
950        },
951        SdfNode::SweepBezier { p0, p1, p2, .. } => SdfNode::SweepBezier {
952            child: new_child,
953            p0: *p0,
954            p1: *p1,
955            p2: *p2,
956        },
957        SdfNode::IcosahedralSymmetry { .. } => SdfNode::IcosahedralSymmetry { child: new_child },
958        SdfNode::IFS {
959            transforms,
960            iterations,
961            ..
962        } => SdfNode::IFS {
963            child: new_child,
964            transforms: transforms.clone(),
965            iterations: *iterations,
966        },
967        SdfNode::HeightmapDisplacement {
968            heightmap,
969            width,
970            height,
971            amplitude,
972            scale,
973            ..
974        } => SdfNode::HeightmapDisplacement {
975            child: new_child,
976            heightmap: heightmap.clone(),
977            width: *width,
978            height: *height,
979            amplitude: *amplitude,
980            scale: *scale,
981        },
982        SdfNode::SurfaceRoughness {
983            frequency,
984            amplitude,
985            octaves,
986            ..
987        } => SdfNode::SurfaceRoughness {
988            child: new_child,
989            frequency: *frequency,
990            amplitude: *amplitude,
991            octaves: *octaves,
992        },
993        SdfNode::Shear { shear, .. } => SdfNode::Shear {
994            child: new_child,
995            shear: *shear,
996        },
997        SdfNode::Animated {
998            speed, amplitude, ..
999        } => SdfNode::Animated {
1000            child: new_child,
1001            speed: *speed,
1002            amplitude: *amplitude,
1003        },
1004        SdfNode::WithMaterial { material_id, .. } => SdfNode::WithMaterial {
1005            child: new_child,
1006            material_id: *material_id,
1007        },
1008
1009        // Leaf nodes have no children; reaching here indicates a bug in replace_at_path
1010        _ => unreachable!("Leaf nodes have no children"),
1011    }
1012}
1013
1014// ── Tests ────────────────────────────────────────────────────
1015
1016#[cfg(test)]
1017mod tests {
1018    use super::*;
1019
1020    #[test]
1021    fn identical_trees_empty_diff() {
1022        let tree = SdfNode::sphere(1.0);
1023        let patch = tree_diff(&tree, &tree);
1024        assert!(patch.ops.is_empty());
1025        assert_eq!(patch.content_hash, 0);
1026    }
1027
1028    #[test]
1029    fn different_primitives() {
1030        let old = SdfNode::sphere(1.0);
1031        let new = SdfNode::sphere(2.0);
1032        let patch = tree_diff(&old, &new);
1033        assert_eq!(patch.ops.len(), 1);
1034        match &patch.ops[0] {
1035            DiffOp::Replace { path, .. } => assert!(path.is_empty()),
1036            _ => panic!("Expected Replace op"),
1037        }
1038    }
1039
1040    #[test]
1041    fn different_primitive_types() {
1042        let old = SdfNode::sphere(1.0);
1043        let new = SdfNode::box3d(1.0, 1.0, 1.0);
1044        let patch = tree_diff(&old, &new);
1045        assert_eq!(patch.ops.len(), 1);
1046    }
1047
1048    #[test]
1049    fn child_changed_in_union() {
1050        let old = SdfNode::sphere(1.0).union(SdfNode::box3d(1.0, 1.0, 1.0));
1051        let new = SdfNode::sphere(2.0).union(SdfNode::box3d(1.0, 1.0, 1.0));
1052        let patch = tree_diff(&old, &new);
1053        // Only the first child changed
1054        assert_eq!(patch.ops.len(), 1);
1055        match &patch.ops[0] {
1056            DiffOp::Replace { path, .. } => assert_eq!(path, &vec![0]),
1057            _ => panic!("Expected Replace op"),
1058        }
1059    }
1060
1061    #[test]
1062    fn apply_patch_roundtrip() {
1063        let old = SdfNode::sphere(1.0).union(SdfNode::box3d(1.0, 1.0, 1.0));
1064        let new = SdfNode::sphere(2.0).union(SdfNode::box3d(1.0, 1.0, 1.0));
1065        let patch = tree_diff(&old, &new);
1066        let result = apply_patch(&old, &patch).unwrap();
1067        assert_eq!(tree_hash(&result), tree_hash(&new));
1068    }
1069
1070    #[test]
1071    fn tree_hash_deterministic() {
1072        let a = SdfNode::sphere(1.0);
1073        let h1 = tree_hash(&a);
1074        let h2 = tree_hash(&a);
1075        assert_eq!(h1, h2);
1076        assert_ne!(h1, 0);
1077    }
1078
1079    #[test]
1080    fn tree_hash_differs() {
1081        let a = SdfNode::sphere(1.0);
1082        let b = SdfNode::sphere(2.0);
1083        assert_ne!(tree_hash(&a), tree_hash(&b));
1084    }
1085
1086    #[test]
1087    fn patch_content_hash_nonempty() {
1088        let old = SdfNode::sphere(1.0);
1089        let new = SdfNode::sphere(2.0);
1090        let patch = tree_diff(&old, &new);
1091        assert_ne!(patch.content_hash, 0);
1092    }
1093
1094    #[test]
1095    fn apply_patch_invalid_path() {
1096        let tree = SdfNode::sphere(1.0);
1097        let patch = TreePatch {
1098            ops: vec![DiffOp::Replace {
1099                path: vec![0], // sphere has no children
1100                old_hash: 0,
1101                new_node: Arc::new(SdfNode::sphere(2.0)),
1102            }],
1103            content_hash: 1,
1104        };
1105        assert!(apply_patch(&tree, &patch).is_err());
1106    }
1107
1108    #[test]
1109    fn apply_patch_hash_mismatch() {
1110        let tree = SdfNode::sphere(1.0);
1111        let patch = TreePatch {
1112            ops: vec![DiffOp::Replace {
1113                path: vec![],
1114                old_hash: 9999, // wrong hash
1115                new_node: Arc::new(SdfNode::sphere(2.0)),
1116            }],
1117            content_hash: 1,
1118        };
1119        match apply_patch(&tree, &patch) {
1120            Err(DiffError::HashMismatch { .. }) => {}
1121            _ => panic!("Expected HashMismatch error"),
1122        }
1123    }
1124
1125    #[test]
1126    fn nested_tree_diff() {
1127        let old = SdfNode::sphere(1.0)
1128            .translate(1.0, 0.0, 0.0)
1129            .union(SdfNode::box3d(1.0, 1.0, 1.0));
1130        let new = SdfNode::sphere(1.0)
1131            .translate(2.0, 0.0, 0.0) // changed offset
1132            .union(SdfNode::box3d(1.0, 1.0, 1.0));
1133        let patch = tree_diff(&old, &new);
1134        assert!(!patch.ops.is_empty());
1135    }
1136
1137    #[test]
1138    fn test_replace_child_smooth_subtraction() {
1139        let old = SdfNode::SmoothSubtraction {
1140            a: Arc::new(SdfNode::sphere(1.0)),
1141            b: Arc::new(SdfNode::box3d(0.5, 0.5, 0.5)),
1142            k: 0.1,
1143        };
1144        let new = SdfNode::SmoothSubtraction {
1145            a: Arc::new(SdfNode::sphere(2.0)), // changed
1146            b: Arc::new(SdfNode::box3d(0.5, 0.5, 0.5)),
1147            k: 0.1,
1148        };
1149        let patch = tree_diff(&old, &new);
1150        assert_eq!(patch.ops.len(), 1);
1151        let result = apply_patch(&old, &patch).unwrap();
1152        assert_eq!(tree_hash(&result), tree_hash(&new));
1153    }
1154
1155    #[test]
1156    fn test_replace_child_morph() {
1157        let old = SdfNode::Morph {
1158            a: Arc::new(SdfNode::sphere(1.0)),
1159            b: Arc::new(SdfNode::box3d(1.0, 1.0, 1.0)),
1160            t: 0.5,
1161        };
1162        let new = SdfNode::Morph {
1163            a: Arc::new(SdfNode::sphere(1.0)),
1164            b: Arc::new(SdfNode::box3d(2.0, 2.0, 2.0)), // changed second child
1165            t: 0.5,
1166        };
1167        let patch = tree_diff(&old, &new);
1168        assert_eq!(patch.ops.len(), 1);
1169        match &patch.ops[0] {
1170            DiffOp::Replace { path, .. } => assert_eq!(path, &vec![1]),
1171            _ => panic!("Expected Replace op"),
1172        }
1173        let result = apply_patch(&old, &patch).unwrap();
1174        assert_eq!(tree_hash(&result), tree_hash(&new));
1175    }
1176
1177    #[test]
1178    fn test_replace_child_xor() {
1179        let old = SdfNode::XOR {
1180            a: Arc::new(SdfNode::sphere(1.0)),
1181            b: Arc::new(SdfNode::sphere(1.5)),
1182        };
1183        let new = SdfNode::XOR {
1184            a: Arc::new(SdfNode::sphere(1.0)),
1185            b: Arc::new(SdfNode::sphere(3.0)), // changed
1186        };
1187        let patch = tree_diff(&old, &new);
1188        assert_eq!(patch.ops.len(), 1);
1189        let result = apply_patch(&old, &patch).unwrap();
1190        assert_eq!(tree_hash(&result), tree_hash(&new));
1191    }
1192
1193    #[test]
1194    fn test_replace_child_complex_tree() {
1195        // SmoothUnion( Groove(sphere, box), StairsUnion(cylinder, torus) )
1196        use glam::Vec3;
1197        let old = SdfNode::SmoothUnion {
1198            a: Arc::new(SdfNode::Groove {
1199                a: Arc::new(SdfNode::sphere(1.0)),
1200                b: Arc::new(SdfNode::box3d(0.5, 0.5, 0.5)),
1201                ra: 0.1,
1202                rb: 0.2,
1203            }),
1204            b: Arc::new(SdfNode::StairsUnion {
1205                a: Arc::new(SdfNode::cylinder(0.5, 2.0)),
1206                b: Arc::new(SdfNode::torus(1.0, 0.2)),
1207                r: 0.3,
1208                n: 4.0,
1209            }),
1210            k: 0.05,
1211        };
1212        // Change the torus minor_radius inside the StairsUnion (path: [1, 1])
1213        let new = SdfNode::SmoothUnion {
1214            a: Arc::new(SdfNode::Groove {
1215                a: Arc::new(SdfNode::sphere(1.0)),
1216                b: Arc::new(SdfNode::box3d(0.5, 0.5, 0.5)),
1217                ra: 0.1,
1218                rb: 0.2,
1219            }),
1220            b: Arc::new(SdfNode::StairsUnion {
1221                a: Arc::new(SdfNode::cylinder(0.5, 2.0)),
1222                b: Arc::new(SdfNode::torus(1.0, 0.5)), // changed
1223                r: 0.3,
1224                n: 4.0,
1225            }),
1226            k: 0.05,
1227        };
1228        let _ = Vec3::ZERO; // suppress unused import warning
1229        let patch = tree_diff(&old, &new);
1230        assert!(!patch.ops.is_empty());
1231        let result = apply_patch(&old, &patch).unwrap();
1232        assert_eq!(tree_hash(&result), tree_hash(&new));
1233    }
1234
1235    #[test]
1236    fn invert_patch_roundtrip() {
1237        let old = SdfNode::sphere(1.0).union(SdfNode::box3d(1.0, 1.0, 1.0));
1238        let new = SdfNode::sphere(2.0).union(SdfNode::box3d(1.0, 1.0, 1.0));
1239        let fwd = tree_diff(&old, &new);
1240        let result = apply_patch(&old, &fwd).unwrap();
1241        let inv = super::invert_patch(&old, &fwd).unwrap();
1242        let restored = apply_patch(&result, &inv).unwrap();
1243        assert_eq!(tree_hash(&restored), tree_hash(&old));
1244    }
1245
1246    #[test]
1247    fn merge_patches_equivalent() {
1248        let a = SdfNode::sphere(1.0).union(SdfNode::box3d(1.0, 1.0, 1.0));
1249        let b = SdfNode::sphere(2.0).union(SdfNode::box3d(1.0, 1.0, 1.0));
1250        let c = SdfNode::sphere(2.0).union(SdfNode::box3d(3.0, 3.0, 3.0));
1251        let p1 = tree_diff(&a, &b);
1252        let p2 = tree_diff(&b, &c);
1253        let merged = super::merge_patches(&a, &p1, &p2).unwrap();
1254        let result = apply_patch(&a, &merged).unwrap();
1255        assert_eq!(tree_hash(&result), tree_hash(&c));
1256    }
1257
1258    #[test]
1259    fn tree_patch_op_count() {
1260        let old = SdfNode::sphere(1.0);
1261        let new = SdfNode::sphere(2.0);
1262        let patch = tree_diff(&old, &new);
1263        assert_eq!(patch.op_count(), 1);
1264        assert!(!patch.is_empty());
1265    }
1266
1267    #[test]
1268    fn empty_patch_is_empty() {
1269        let tree = SdfNode::sphere(1.0);
1270        let patch = tree_diff(&tree, &tree);
1271        assert!(patch.is_empty());
1272        assert_eq!(patch.op_count(), 0);
1273    }
1274
1275    #[test]
1276    fn insert_op_wraps_node() {
1277        let tree = SdfNode::sphere(1.0);
1278        let wrapper = SdfNode::Translate {
1279            child: Arc::new(SdfNode::sphere(0.0)), // placeholder
1280            offset: glam::Vec3::new(1.0, 0.0, 0.0),
1281        };
1282        let patch = TreePatch {
1283            ops: vec![DiffOp::Insert {
1284                path: vec![],
1285                old_hash: tree_hash(&tree),
1286                wrapper: Arc::new(wrapper),
1287            }],
1288            content_hash: 1,
1289        };
1290        let result = apply_patch(&tree, &patch).unwrap();
1291        match result {
1292            SdfNode::Translate { ref child, offset } => {
1293                assert_eq!(tree_hash(child), tree_hash(&tree));
1294                assert_eq!(offset, glam::Vec3::new(1.0, 0.0, 0.0));
1295            }
1296            _ => panic!("Expected Translate wrapper"),
1297        }
1298    }
1299
1300    #[test]
1301    fn delete_op_unwraps_node() {
1302        let inner = SdfNode::sphere(1.0);
1303        let tree = SdfNode::Translate {
1304            child: Arc::new(inner.clone()),
1305            offset: glam::Vec3::new(1.0, 0.0, 0.0),
1306        };
1307        let patch = TreePatch {
1308            ops: vec![DiffOp::Delete {
1309                path: vec![],
1310                old_hash: tree_hash(&tree),
1311                child_index: 0,
1312            }],
1313            content_hash: 1,
1314        };
1315        let result = apply_patch(&tree, &patch).unwrap();
1316        assert_eq!(tree_hash(&result), tree_hash(&inner));
1317    }
1318
1319    #[test]
1320    fn delete_invalid_child_index() {
1321        let tree = SdfNode::sphere(1.0); // leaf, no children
1322        let patch = TreePatch {
1323            ops: vec![DiffOp::Delete {
1324                path: vec![],
1325                old_hash: tree_hash(&tree),
1326                child_index: 0,
1327            }],
1328            content_hash: 1,
1329        };
1330        assert!(apply_patch(&tree, &patch).is_err());
1331    }
1332
1333    #[test]
1334    fn test_replace_child_single_child_modifiers() {
1335        // Test Bend, Elongate, Mirror roundtrips
1336        use glam::Vec3;
1337
1338        // Bend
1339        let old_bend = SdfNode::Bend {
1340            child: Arc::new(SdfNode::sphere(1.0)),
1341            curvature: 0.5,
1342        };
1343        let new_bend = SdfNode::Bend {
1344            child: Arc::new(SdfNode::sphere(2.0)),
1345            curvature: 0.5,
1346        };
1347        let patch = tree_diff(&old_bend, &new_bend);
1348        assert_eq!(patch.ops.len(), 1);
1349        let result = apply_patch(&old_bend, &patch).unwrap();
1350        assert_eq!(tree_hash(&result), tree_hash(&new_bend));
1351
1352        // Elongate
1353        let old_elongate = SdfNode::Elongate {
1354            child: Arc::new(SdfNode::sphere(1.0)),
1355            amount: Vec3::new(0.5, 0.5, 0.5),
1356        };
1357        let new_elongate = SdfNode::Elongate {
1358            child: Arc::new(SdfNode::sphere(3.0)),
1359            amount: Vec3::new(0.5, 0.5, 0.5),
1360        };
1361        let patch2 = tree_diff(&old_elongate, &new_elongate);
1362        assert_eq!(patch2.ops.len(), 1);
1363        let result2 = apply_patch(&old_elongate, &patch2).unwrap();
1364        assert_eq!(tree_hash(&result2), tree_hash(&new_elongate));
1365
1366        // Mirror
1367        let old_mirror = SdfNode::Mirror {
1368            child: Arc::new(SdfNode::box3d(1.0, 1.0, 1.0)),
1369            axes: Vec3::new(1.0, 0.0, 0.0),
1370        };
1371        let new_mirror = SdfNode::Mirror {
1372            child: Arc::new(SdfNode::box3d(2.0, 2.0, 2.0)),
1373            axes: Vec3::new(1.0, 0.0, 0.0),
1374        };
1375        let patch3 = tree_diff(&old_mirror, &new_mirror);
1376        assert_eq!(patch3.ops.len(), 1);
1377        let result3 = apply_patch(&old_mirror, &patch3).unwrap();
1378        assert_eq!(tree_hash(&result3), tree_hash(&new_mirror));
1379    }
1380}