Skip to main content

ranim_items/
hierarchy.rs

1//! Hierarchical scene-graph composition for items.
2//!
3//! This module lifts
4//! [`Transformed`](ranim_core::core_item::transformed::Transformed)'s
5//! "canonical local data + external transform" model from a flat wrapper to
6//! a tree. Scene-graph composition lives in the items layer, built on top of
7//! ranim-core's transform-group algebra
8//! ([`TransformGroup`](ranim_core::traits::TransformGroup)): each node
9//! stores canonical local data (like
10//! [`Transformed::inner`](ranim_core::core_item::transformed::Transformed::inner)
11//! does) plus one transform from its local space into the parent's space.
12//!
13//! The structural rules of the model:
14//!
15//! - **Extraction flattens depth-first** and composes matrices per node,
16//!   `acc = acc * node_transform`, so the resulting
17//!   [`CoreItem`](ranim_core::core_item::CoreItem) sequence preserves
18//!   painter's-algorithm draw order (front-to-back document order).
19//! - Every node's pose interpolates **independently** of its geometry:
20//!   lerping moves transforms and leaf payloads only and never bakes points
21//!   into vertices — the basis for future skeletal animation.
22//!
23//! # Examples
24//!
25//! ```rust
26//! use ranim_core::core_item::transformed::{Transformed, TransformedExt};
27//! use ranim_core::core_item::vitem::VItem as CoreVItem;
28//! use ranim_core::glam::{DAffine3, Vec4, dvec3};
29//! use ranim_core::traits::ShiftTransform;
30//! use ranim_core::Extract;
31//! use ranim_items::hierarchy::Node;
32//!
33//! let stroke = CoreVItem {
34//!     points: vec![Vec4::new(1.0, 0.0, 0.0, 0.0)],
35//!     ..Default::default()
36//! };
37//! // A tree is placed by wrapping it — root posing is O(1) and never
38//! // bakes points.
39//! let mut tree = Transformed::new(
40//!     Node::<CoreVItem>::group(vec![
41//!         Node::leaf(stroke.clone()).with_id("a"),
42//!         Node::leaf(stroke).with_id("b"),
43//!     ]),
44//!     DAffine3::IDENTITY,
45//! );
46//! tree.shift(dvec3(1.0, 0.0, 0.0));
47//!
48//! let extracted = tree.extract();
49//! assert_eq!(extracted.len(), 2);
50//! ```
51
52use std::fmt;
53use std::ops::Range;
54
55use ranim_core::components::width::Width;
56use ranim_core::core_item::transformed::Transformed;
57use ranim_core::{
58    Extract,
59    anchor::{Aabb, Centroid, Locate},
60    color::{AlphaColor, Srgb, palette::css},
61    core_item::CoreItem,
62    glam::{DAffine3, DVec3},
63    traits::{
64        Alignable, Empty, FillColor, Interpolatable, Opacity, Partial, StrokeColor, StrokeWidth,
65        TransformGroup,
66    },
67    utils::resize_preserving_order_with_repeated_indices,
68};
69use tracing::warn;
70
71/// A node of a scene-graph tree: pure structure — an external id, an
72/// optional payload, and a list of *placed* children.
73///
74/// Placement is not stored on the node: each child sits inside a
75/// [`Transformed`] wrapper carrying its local-to-parent transform, so the
76/// doctrine "placement lives in `Transformed` only" holds for trees too.
77/// All pose algebra (composition, lerp, AABB corners, widening, root
78/// posing) comes from [`Transformed`]'s own implementations instead of
79/// being duplicated here, and a whole tree is placed by wrapping it:
80/// `Transformed::new(tree, pose)` or `tree.transformed(pose)`.
81///
82/// This is the same division as reading a glTF scene structurally: the
83/// node's `name`/`mesh` map to the payload and id, while its `matrix|TRS`
84/// maps to the wrapper around the node.
85///
86/// # Examples
87///
88/// Build trees with [`Node::leaf`], [`Node::group`], [`Node::branch`], and
89/// the builders; place a child with [`transformed`](ranim_core::core_item::transformed::TransformedExt::transformed):
90///
91/// ```
92/// use ranim_core::core_item::transformed::TransformedExt;
93/// use ranim_core::glam::dvec3;
94/// use ranim_core::traits::Translation;
95/// use ranim_items::hierarchy::Node;
96///
97/// let tree = Node {
98///     id: Some("outer".into()),
99///     item: None,
100///     children: vec![
101///         Node::leaf("geometry".to_string())
102///             .transformed(Translation(dvec3(1.0, 0.0, 0.0))),
103///     ],
104/// };
105/// assert_eq!(tree.children[0].inner.item(), Some(&"geometry".to_string()));
106/// ```
107pub struct Node<I, G = DAffine3> {
108    /// External identifier carried from the source format (e.g. SVG element
109    /// id, glTF node name). Ignored by rendering/extraction beyond being
110    /// transported: alignment preserves it, and lerping switches ids at the
111    /// mid-point exactly like other front-loaded fields in ranim.
112    pub id: Option<String>,
113    /// The payload carried by this node, in canonical local coordinates.
114    pub item: Option<I>,
115    /// The placed child nodes, living in this node's local space. Stored in
116    /// source order; extraction preserves that order depth-first so
117    /// downstream consumers see painter's-algorithm ordering, and a node's
118    /// own payload paints before its descendants.
119    pub children: Vec<Transformed<Node<I, G>, G>>,
120}
121
122// MARK: Manual basic impls
123//
124// Derived impls would place bounds on the defaulted type parameter `G` even
125// where the fields do not require them; writing these by hand keeps the
126// bounds to exactly what the fields demand.
127
128impl<I: Clone, G: Clone> Clone for Node<I, G> {
129    fn clone(&self) -> Self {
130        Self {
131            id: self.id.clone(),
132            item: self.item.clone(),
133            children: self.children.clone(),
134        }
135    }
136}
137
138impl<I: PartialEq, G: PartialEq> PartialEq for Node<I, G> {
139    fn eq(&self, other: &Self) -> bool {
140        self.id == other.id && self.item == other.item && self.children == other.children
141    }
142}
143
144impl<I: Eq, G: Eq> Eq for Node<I, G> {}
145
146impl<I: fmt::Debug, G: fmt::Debug> fmt::Debug for Node<I, G> {
147    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148        f.debug_struct("Node")
149            .field("id", &self.id)
150            .field("item", &self.item)
151            .field("children", &self.children)
152            .finish()
153    }
154}
155
156// MARK: Inherent API
157
158impl<I, G> Node<I, G> {
159    /// Pair a payload (if any) with placed children, without an external id.
160    pub fn new(item: Option<I>, children: Vec<Transformed<Self, G>>) -> Self {
161        Self {
162            id: None,
163            item,
164            children,
165        }
166    }
167
168    /// Attach an external id, consuming and returning the node.
169    #[must_use]
170    pub fn with_id(mut self, id: impl Into<String>) -> Self {
171        self.id = Some(id.into());
172        self
173    }
174
175    /// Whether this node is a bare leaf: a payload with no children.
176    pub fn is_leaf(&self) -> bool {
177        self.item.is_some() && self.children.is_empty()
178    }
179
180    /// Whether this node is a pure frame: no payload, only (possibly empty)
181    /// children.
182    pub fn is_group(&self) -> bool {
183        self.item.is_none()
184    }
185
186    /// The payload carried by this node, if any.
187    pub fn item(&self) -> Option<&I> {
188        self.item.as_ref()
189    }
190
191    /// The payload mutably, if any.
192    pub fn item_mut(&mut self) -> Option<&mut I> {
193        self.item.as_mut()
194    }
195
196    /// The placed children.
197    pub fn children(&self) -> &[Transformed<Self, G>] {
198        &self.children
199    }
200
201    /// The placed children mutably.
202    pub fn children_mut(&mut self) -> &mut [Transformed<Self, G>] {
203        &mut self.children
204    }
205
206    /// The first leaf payload in depth-first order, without composing any
207    /// transforms. This backs the color/stroke-width getters; `None` means
208    /// the tree has no payloads at all.
209    pub fn first_leaf(&self) -> Option<&I> {
210        let mut stack = vec![self];
211        while let Some(node) = stack.pop() {
212            if let Some(item) = &node.item {
213                return Some(item);
214            }
215            stack.extend(node.children.iter().rev().map(|child| &child.inner));
216        }
217        None
218    }
219
220    /// Look up a *placement* by walking child indices: `[i]` returns child
221    /// `i` (including its pose), `[i, j]` returns child `j` of child `i`'s
222    /// frame, and so on. Paths must be non-empty — the receiver is the
223    /// frame itself, not a placement — and `None` is returned when an index
224    /// is out of bounds.
225    pub fn get(&self, path: &[usize]) -> Option<&Transformed<Self, G>> {
226        let (first, rest) = path.split_first()?;
227        let child = self.children.get(*first)?;
228        if rest.is_empty() {
229            Some(child)
230        } else {
231            child.inner.get(rest)
232        }
233    }
234
235    /// Mutable variant of [`Node::get`].
236    pub fn get_mut(&mut self, path: &[usize]) -> Option<&mut Transformed<Self, G>> {
237        let (first, rest) = path.split_first()?;
238        let child = self.children.get_mut(*first)?;
239        if rest.is_empty() {
240            Some(child)
241        } else {
242            child.inner.get_mut(rest)
243        }
244    }
245
246    /// The first placement (depth-first preorder) whose id equals `id`.
247    ///
248    /// Ids are external labels (SVG ids, glTF node names) and are not
249    /// guaranteed unique, so duplicates resolve to the preorder-first match;
250    /// see [`Node::by_ids`] for every match and [`Node::by_id_path`] for a
251    /// reusable address. Placements are searched, not the receiver — the
252    /// receiver is the frame they live in. `None` when no placement carries
253    /// the id.
254    pub fn by_id(&self, id: &str) -> Option<&Transformed<Self, G>> {
255        self.children.iter().find_map(|child| {
256            if child.inner.id.as_deref() == Some(id) {
257                Some(child)
258            } else {
259                child.inner.by_id(id)
260            }
261        })
262    }
263
264    /// Mutable variant of [`Node::by_id`].
265    pub fn by_id_mut(&mut self, id: &str) -> Option<&mut Transformed<Self, G>> {
266        self.children.iter_mut().find_map(|child| {
267            if child.inner.id.as_deref() == Some(id) {
268                Some(child)
269            } else {
270                child.inner.by_id_mut(id)
271            }
272        })
273    }
274
275    /// Every placement whose id equals `id`, in depth-first order.
276    pub fn by_ids(&self, id: &str) -> Vec<&Transformed<Self, G>> {
277        let mut matches = Vec::new();
278        self.collect_by_id(id, &mut matches);
279        matches
280    }
281
282    fn collect_by_id<'a>(&'a self, id: &str, matches: &mut Vec<&'a Transformed<Self, G>>) {
283        for child in &self.children {
284            if child.inner.id.as_deref() == Some(id) {
285                matches.push(child);
286            }
287            child.inner.collect_by_id(id, matches);
288        }
289    }
290
291    /// The index path (see [`Node::get`]) of the first placement in
292    /// depth-first order whose id equals `id`. Useful to reuse an address
293    /// across frames without re-searching.
294    pub fn by_id_path(&self, id: &str) -> Option<Vec<usize>> {
295        for (index, child) in self.children.iter().enumerate() {
296            if child.inner.id.as_deref() == Some(id) {
297                return Some(vec![index]);
298            }
299            if let Some(mut path) = child.inner.by_id_path(id) {
300                path.insert(0, index);
301                return Some(path);
302            }
303        }
304        None
305    }
306
307    /// The first leaf payload in depth-first order, mutably.
308    pub fn first_leaf_mut(&mut self) -> Option<&mut I> {
309        if let Some(item) = self.item.as_mut() {
310            return Some(item);
311        }
312        self.children
313            .iter_mut()
314            .find_map(|child| child.inner.first_leaf_mut())
315    }
316
317    /// Iterate over flattened leaf payloads with their accumulated world
318    /// affine, yielding `(world_affine, &item)` pairs in depth-first order —
319    /// i.e. painter's-algorithm draw order.
320    ///
321    /// The world affine composes top-down through every placement's
322    /// transform, starting from the identity at the receiver: the receiver
323    /// is an unplaced frame, so wrapping the tree in [`Transformed`] places
324    /// it (see [`PlacedLeaves`]). The same placement [`Extract`] composes
325    /// into core items. Implemented with an explicit stack, so deep trees
326    /// cannot overflow the call stack.
327    pub fn leaves(&self) -> Leaves<'_, I, G>
328    where
329        G: Clone + Into<DAffine3>,
330    {
331        Leaves {
332            stack: vec![(DAffine3::IDENTITY, self)],
333        }
334    }
335
336    /// Iterate over mutable references to all leaf payloads in depth-first
337    /// order. Unlike [`Node::leaves`], no transforms are composed: callers
338    /// mutate canonical local data only.
339    pub fn leaves_mut(&mut self) -> LeavesMut<'_, I, G> {
340        LeavesMut { stack: vec![self] }
341    }
342
343    /// Map every leaf payload to a new type, keeping ids, poses, and the
344    /// tree shape unchanged. This is the recursive analog of
345    /// [`Transformed::map_inner`].
346    pub fn map_inner<U>(self, f: impl FnMut(I) -> U) -> Node<U, G> {
347        fn map_inner_rec<I, U, G>(node: Node<I, G>, mut f: &mut impl FnMut(I) -> U) -> Node<U, G> {
348            let Node { id, item, children } = node;
349            Node {
350                id,
351                item: item.map(&mut f),
352                children: children
353                    .into_iter()
354                    .map(|child| Transformed::new(map_inner_rec(child.inner, f), child.transform))
355                    .collect(),
356            }
357        }
358        let mut f = f;
359        map_inner_rec(self, &mut f)
360    }
361
362    /// Map the transform storage of every placement to a new type while
363    /// keeping everything else unchanged. This mirrors
364    /// [`Transformed::map_transform`] and is the general form of converting
365    /// between transform groups — including widening, which intentionally
366    /// has no blanket `From` impl on `Node`.
367    pub fn map_transform<H>(self, f: impl FnMut(G) -> H) -> Node<I, H> {
368        fn map_transform_rec<I, G, H>(node: Node<I, G>, f: &mut impl FnMut(G) -> H) -> Node<I, H> {
369            let Node { id, item, children } = node;
370            Node {
371                id,
372                item,
373                children: children
374                    .into_iter()
375                    .map(|child| {
376                        Transformed::new(map_transform_rec(child.inner, f), f(child.transform))
377                    })
378                    .collect(),
379            }
380        }
381        let mut f = f;
382        map_transform_rec(self, &mut f)
383    }
384}
385
386impl<I, G: TransformGroup> Node<I, G> {
387    /// Create a bare leaf — a payload with no children — without an id.
388    pub fn leaf(item: I) -> Self {
389        Self {
390            id: None,
391            item: Some(item),
392            children: Vec::new(),
393        }
394    }
395
396    /// Create a pure frame — no payload — holding the placed children.
397    /// Plain nodes passed in the iterator place with the identity pose.
398    pub fn group(children: impl IntoIterator<Item = impl Into<Transformed<Self, G>>>) -> Self {
399        Self {
400            id: None,
401            item: None,
402            children: children.into_iter().map(Into::into).collect(),
403        }
404    }
405
406    /// Create a branch — a payload and placed children — without an id. The
407    /// payload paints before the children. Plain nodes passed in the
408    /// iterator place with the identity pose.
409    pub fn branch(
410        item: I,
411        children: impl IntoIterator<Item = impl Into<Transformed<Self, G>>>,
412    ) -> Self {
413        Self {
414            id: None,
415            item: Some(item),
416            children: children.into_iter().map(Into::into).collect(),
417        }
418    }
419
420    /// Create an empty pure anchor frame: no payload, no children.
421    pub fn frame() -> Self {
422        Self {
423            id: None,
424            item: None,
425            children: Vec::new(),
426        }
427    }
428}
429
430// MARK: Conversions
431
432/// A bare `Node` places with the identity pose, so plain and wrapped nodes
433/// mix freely in the same `vec![...]` of children.
434impl<I, G: TransformGroup> From<Node<I, G>> for Transformed<Node<I, G>, G> {
435    fn from(inner: Node<I, G>) -> Self {
436        Transformed::new(inner, G::identity())
437    }
438}
439
440// MARK: Iterators
441
442/// Iterator over flattened leaf payloads with accumulated world affines,
443/// produced by [`Node::leaves`].
444///
445/// Yields `(world_affine, &item)` pairs in depth-first (painter's algorithm)
446/// order.
447pub struct Leaves<'a, I, G = DAffine3> {
448    stack: Vec<(DAffine3, &'a Node<I, G>)>,
449}
450
451impl<'a, I, G> Iterator for Leaves<'a, I, G>
452where
453    G: Clone + Into<DAffine3>,
454{
455    type Item = (DAffine3, &'a I);
456
457    fn next(&mut self) -> Option<Self::Item> {
458        while let Some((acc, node)) = self.stack.pop() {
459            // A node may carry a payload *and* children: queue the placements
460            // first (in reverse, so popping yields source order), then yield
461            // the payload — painter's-algorithm order with the payload
462            // painting before its descendants.
463            for child in node.children.iter().rev() {
464                let acc_child = acc.compose(&child.transform.clone().into());
465                self.stack.push((acc_child, &child.inner));
466            }
467            if let Some(item) = &node.item {
468                return Some((acc, item));
469            }
470        }
471        None
472    }
473}
474
475/// Iterator over mutable references to leaf payloads, produced by
476/// [`Node::leaves_mut`]. Depth-first order; no transforms are composed.
477pub struct LeavesMut<'a, I, G = DAffine3> {
478    stack: Vec<&'a mut Node<I, G>>,
479}
480
481impl<'a, I, G> Iterator for LeavesMut<'a, I, G> {
482    type Item = &'a mut I;
483
484    fn next(&mut self) -> Option<Self::Item> {
485        while let Some(node) = self.stack.pop() {
486            self.stack
487                .extend(node.children.iter_mut().rev().map(|child| &mut child.inner));
488            if let Some(item) = node.item.as_mut() {
489                return Some(item);
490            }
491        }
492        None
493    }
494}
495
496// MARK: Placed trees
497
498/// Leaf iteration for a *placed* tree: the wrapper's pose seeds the
499/// accumulation. A bare [`Node`] iterates from the identity via
500/// [`Node::leaves`].
501pub trait PlacedLeaves<I, G>
502where
503    G: Clone + Into<DAffine3>,
504{
505    /// Iterate the placed tree's leaf payloads with accumulated world
506    /// affines (see [`Node::leaves`]).
507    fn leaves(&self) -> Leaves<'_, I, G>;
508
509    /// Mutable variant (no transforms are composed).
510    fn leaves_mut(&mut self) -> LeavesMut<'_, I, G>;
511}
512
513impl<I, G> PlacedLeaves<I, G> for Transformed<Node<I, G>, G>
514where
515    G: Clone + Into<DAffine3>,
516{
517    fn leaves(&self) -> Leaves<'_, I, G> {
518        Leaves {
519            stack: vec![(self.transform.clone().into(), &self.inner)],
520        }
521    }
522
523    fn leaves_mut(&mut self) -> LeavesMut<'_, I, G> {
524        LeavesMut {
525            stack: vec![&mut self.inner],
526        }
527    }
528}
529
530// MARK: Extract
531
532impl<I, G> Extract for Node<I, G>
533where
534    I: Extract<Target = CoreItem>,
535    G: Clone + Into<DAffine3>,
536{
537    type Target = CoreItem;
538
539    fn extract_into(&self, buf: &mut Vec<Self::Target>) {
540        if let Some(item) = &self.item {
541            item.extract_into(buf);
542        }
543        // Placements extract their own subtree and compose their own pose
544        // onto the appended slice (via `Transformed`'s `Extract`), so a
545        // chain composes as `t_root * ... * t_leaf * local`, emission stays
546        // depth-first, and a node's own payload paints before its
547        // descendants.
548        for child in &self.children {
549            child.extract_into(buf);
550        }
551    }
552}
553
554// MARK: Interpolatable
555
556impl<I, G> Interpolatable for Node<I, G>
557where
558    I: Interpolatable,
559    G: Interpolatable,
560{
561    /// Structural lerp: nodes interpolate positionally, payloads and node
562    /// poses interpolate independently, and ids switch at the mid-point like
563    /// other front-loaded fields in ranim. Callers must have aligned
564    /// structures first (see [`Alignable`]): payload-presence mismatches
565    /// panic, and unequal sibling counts follow `Vec`'s truncating-zip
566    /// precedent.
567    fn lerp(&self, target: &Self, t: f64) -> Self {
568        Self {
569            id: if t < 0.5 {
570                self.id.clone()
571            } else {
572                target.id.clone()
573            },
574            item: lerp_items(&self.item, &target.item, t),
575            children: self
576                .children
577                .iter()
578                .zip(target.children.iter())
579                .map(|(current, target)| current.lerp(target, t))
580                .collect(),
581        }
582    }
583}
584
585/// Structural lerp over payloads. `None` must pair with `None`: a presence
586/// mismatch is filled with transparent clones by [`Alignable`] first.
587fn lerp_items<I: Interpolatable>(current: &Option<I>, target: &Option<I>, t: f64) -> Option<I> {
588    match (current, target) {
589        (Some(current), Some(target)) => Some(current.lerp(target, t)),
590        (None, None) => None,
591        _ => panic!("interpolating unaligned hierarchies: align them with Alignable first"),
592    }
593}
594
595// MARK: Alignable
596
597impl<I, G> Alignable for Node<I, G>
598where
599    I: Alignable + Opacity,
600    G: Clone,
601{
602    /// Whether both sides are already structurally compatible for direct
603    /// interpolation: payload presence must match positionally, sibling
604    /// counts must be equal, and every payload and child pair must satisfy
605    /// [`Alignable::is_aligned`]. This mirrors the `Vec<T>` blanket's
606    /// pre-alignment contract; [`Alignable::align_with`] establishes this
607    /// state from mismatched trees.
608    fn is_aligned(&self, other: &Self) -> bool {
609        let items_aligned = match (&self.item, &other.item) {
610            (Some(current), Some(target)) => current.is_aligned(target),
611            (None, None) => true,
612            _ => false,
613        };
614        items_aligned
615            && self.children.len() == other.children.len()
616            && self
617                .children
618                .iter()
619                .zip(other.children.iter())
620                .all(|(current, target)| current.is_aligned(target))
621    }
622
623    /// Align two trees for interpolation under one uniform rule: **absence
624    /// is filled with a transparent clone of the present side**.
625    ///
626    /// 1. **Payload presence**: when only one side carries an item, the
627    ///    other side receives a transparent (`set_opacity(0.0)`) clone of
628    ///    it, so lerping fades the payload in or out smoothly instead of
629    ///    jumping.
630    /// 2. **Payload pairs**: when both sides carry items, they align with
631    ///    each other (vertex-level padding for point data).
632    /// 3. **Children**: unequal child counts are padded on both sides. A
633    ///    non-empty list grows by repeating its own entries
634    ///    (`resize_preserving_order_with_repeated_indices`, matching the
635    ///    `Vec<T>: Alignable` blanket); an *empty* list has nothing of its
636    ///    own to repeat, so it grows with transparent clones of the other
637    ///    side's children — the same absence rule as payloads. Pairs
638    ///    recurse.
639    fn align_with(&mut self, other: &mut Self) {
640        match (self.item.as_mut(), other.item.as_mut()) {
641            (None, Some(target_item)) => {
642                let mut fill = target_item.clone();
643                fill.set_opacity(0.0);
644                self.item = Some(fill);
645            }
646            (Some(current_item), None) => {
647                let mut fill = current_item.clone();
648                fill.set_opacity(0.0);
649                other.item = Some(fill);
650            }
651            _ => {}
652        }
653        if let (Some(current), Some(target)) = (&mut self.item, &mut other.item) {
654            current.align_with(target);
655        }
656        let len = self.children.len().max(other.children.len());
657        match (self.children.len(), other.children.len()) {
658            (0, 0) => {}
659            (0, _) => self.children = transparent_clones(&other.children, len),
660            (_, 0) => other.children = transparent_clones(&self.children, len),
661            _ => {
662                expand_with_transparent_repeats(&mut self.children, len);
663                expand_with_transparent_repeats(&mut other.children, len);
664            }
665        }
666        self.children
667            .iter_mut()
668            .zip(other.children.iter_mut())
669            .for_each(|(current, target)| current.align_with(target));
670    }
671}
672
673/// Expand `nodes` in place to `len` entries, preserving order; repeated
674/// stand-ins become fully transparent via `set_opacity(0.0)`, matching the
675/// `Vec<T>` align blanket in ranim-core.
676fn expand_with_transparent_repeats<I, G>(nodes: &mut Vec<Transformed<Node<I, G>, G>>, len: usize)
677where
678    I: Opacity + Clone,
679    G: Clone,
680{
681    if nodes.len() != len {
682        let (mut expanded, repeated_idxs) =
683            resize_preserving_order_with_repeated_indices(nodes, len);
684        for idx in repeated_idxs {
685            expanded[idx].set_opacity(0.0);
686        }
687        *nodes = expanded;
688    }
689}
690
691/// Grow `source` to `len` entries by cloning entries and marking every
692/// clone transparent — the absence rule of [`Alignable::align_with`] for
693/// child lists that have nothing of their own to repeat.
694fn transparent_clones<I, G>(
695    source: &[Transformed<Node<I, G>, G>],
696    len: usize,
697) -> Vec<Transformed<Node<I, G>, G>>
698where
699    I: Opacity + Clone,
700    G: Clone,
701{
702    source
703        .iter()
704        .cycle()
705        .take(len)
706        .map(|child| {
707            let mut fill = child.clone();
708            fill.set_opacity(0.0);
709            fill
710        })
711        .collect()
712}
713
714// MARK: Partial
715
716impl<I, G> Partial for Node<I, G>
717where
718    I: Partial,
719    G: Clone,
720{
721    fn get_partial(&self, range: Range<f64>) -> Self {
722        Self {
723            id: self.id.clone(),
724            item: self
725                .item
726                .as_ref()
727                .map(|item| item.get_partial(range.clone())),
728            children: self
729                .children
730                .iter()
731                .map(|child| child.get_partial(range.clone()))
732                .collect(),
733        }
734    }
735
736    fn get_partial_closed(&self, range: Range<f64>) -> Self {
737        Self {
738            id: self.id.clone(),
739            item: self
740                .item
741                .as_ref()
742                .map(|item| item.get_partial_closed(range.clone())),
743            children: self
744                .children
745                .iter()
746                .map(|child| child.get_partial_closed(range.clone()))
747                .collect(),
748        }
749    }
750}
751
752// MARK: Empty
753
754impl<I, G> Empty for Node<I, G>
755where
756    I: Empty,
757{
758    fn empty() -> Self {
759        Node {
760            id: None,
761            // A payload of empty geometry (not `None`), so an `Empty`-seeded
762            // interpolation has a payload position to fade through — parity
763            // with the old leaf-only shape.
764            item: Some(I::empty()),
765            children: Vec::new(),
766        }
767    }
768}
769
770// MARK: Aabb
771
772impl<I, G> Aabb for Node<I, G>
773where
774    I: Aabb,
775    G: Clone + Into<DAffine3>,
776{
777    /// Union of the payload's and the placed children's AABBs — each
778    /// placement's wrapper already applies its own pose, so the result is
779    /// in the receiver's local frame. A node with no payload and no
780    /// children warns and reports a degenerate box, mirroring the slice
781    /// impl in ranim-core.
782    fn aabb(&self) -> [DVec3; 2] {
783        let mut inner_box: Option<[DVec3; 2]> = self.item.as_ref().map(Aabb::aabb);
784        for child in &self.children {
785            let [lo, hi] = child.aabb();
786            inner_box = Some(match inner_box {
787                Some([acc_lo, acc_hi]) => [acc_lo.min(lo), acc_hi.max(hi)],
788                None => [lo, hi],
789            });
790        }
791        inner_box.unwrap_or_else(|| {
792            warn!("Empty bounding box, is the tree empty?");
793            [DVec3::ZERO, DVec3::ZERO]
794        })
795    }
796}
797
798// MARK: Locate
799
800/// The centroid of a tree weights every flattened leaf equally: sum each
801/// leaf's centroid mapped through its accumulated world affine and divide by
802/// the leaf count — NOT per-child weighting. An empty tree warns and returns
803/// zero instead of producing NaNs.
804impl<I, G> Locate<Node<I, G>> for Centroid
805where
806    Centroid: Locate<I>,
807    G: Clone + Into<DAffine3>,
808{
809    fn locate(&self, target: &Node<I, G>) -> DVec3 {
810        let mut sum = DVec3::ZERO;
811        let mut count = 0usize;
812        for (affine, leaf) in target.leaves() {
813            sum += affine.transform_point3(self.locate(leaf));
814            count += 1;
815        }
816        if count == 0 {
817            warn!("Locating the centroid of an empty tree, returning zero");
818            return DVec3::ZERO;
819        }
820        sum / count as f64
821    }
822}
823
824// MARK: Opacity
825
826impl<I, G> Opacity for Node<I, G>
827where
828    I: Opacity,
829{
830    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
831        for leaf in self.leaves_mut() {
832            leaf.set_opacity(opacity);
833        }
834        self
835    }
836}
837
838// MARK: FillColor
839
840impl<I, G> FillColor for Node<I, G>
841where
842    I: FillColor,
843{
844    /// The fill color of the first leaf in DFS order; an empty tree warns
845    /// and reports white.
846    fn fill_color(&self) -> AlphaColor<Srgb> {
847        self.first_leaf()
848            .map(FillColor::fill_color)
849            .unwrap_or_else(|| {
850                warn!("Accessing the fill color of an empty tree, returning white");
851                css::WHITE
852            })
853    }
854
855    fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
856        for leaf in self.leaves_mut() {
857            leaf.set_fill_color(color);
858        }
859        self
860    }
861
862    fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
863        for leaf in self.leaves_mut() {
864            leaf.set_fill_opacity(opacity);
865        }
866        self
867    }
868}
869
870// MARK: StrokeColor
871
872impl<I, G> StrokeColor for Node<I, G>
873where
874    I: StrokeColor,
875{
876    /// The stroke color of the first leaf in DFS order; an empty tree warns
877    /// and reports white.
878    fn stroke_color(&self) -> AlphaColor<Srgb> {
879        self.first_leaf()
880            .map(StrokeColor::stroke_color)
881            .unwrap_or_else(|| {
882                warn!("Accessing the stroke color of an empty tree, returning white");
883                css::WHITE
884            })
885    }
886
887    fn set_stroke_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
888        for leaf in self.leaves_mut() {
889            leaf.set_stroke_color(color);
890        }
891        self
892    }
893
894    fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
895        for leaf in self.leaves_mut() {
896            leaf.set_stroke_opacity(opacity);
897        }
898        self
899    }
900}
901
902// MARK: StrokeWidth
903
904impl<I, G> StrokeWidth for Node<I, G>
905where
906    I: StrokeWidth,
907{
908    /// The stroke width of the first leaf in DFS order; an empty tree warns
909    /// and reports `0.0`.
910    fn stroke_width(&self) -> f32 {
911        self.first_leaf()
912            .map(StrokeWidth::stroke_width)
913            .unwrap_or_else(|| {
914                warn!("Accessing the stroke width of an empty tree, returning 0");
915                0.0
916            })
917    }
918
919    /// Forward the stroke-width function to every leaf independently.
920    fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [Width])) -> &mut Self {
921        for leaf in self.leaves_mut() {
922            leaf.apply_stroke_func(&f);
923        }
924        self
925    }
926}
927
928#[cfg(test)]
929mod tests {
930    use super::*;
931    use crate::hierarchy::PlacedLeaves;
932    use ranim_core::Extract;
933    use ranim_core::core_item::transformed::{Transformed, TransformedExt};
934    use ranim_core::core_item::vitem::VItem as CoreVItem;
935    use ranim_core::glam::{DQuat, Mat4, Quat, Vec3, Vec4, dvec3};
936    use ranim_core::traits::{ApplyTransform, Rigid, Similarity, Translation};
937
938    type CoreNode<G = DAffine3> = Node<CoreVItem, G>;
939    type HierarchyVItem = crate::vitem::VItem;
940
941    /// A core VItem whose single anchor carries a marker on the x axis, so
942    /// tests can identify which leaf was emitted.
943    fn marked_core_vitem(marker: f32) -> CoreVItem {
944        CoreVItem {
945            points: vec![Vec4::new(marker, 0.0, 0.0, 0.0)],
946            ..Default::default()
947        }
948    }
949
950    /// A high-level VItem with opaque strokes, used for opacity inspection.
951    fn stroked_vitem(marker: f64) -> HierarchyVItem {
952        let mut vitem = HierarchyVItem::from_vpoints(vec![
953            dvec3(marker, 0.0, 0.0),
954            dvec3(marker + 0.5, 0.0, 0.0),
955            dvec3(marker + 1.0, 0.0, 0.0),
956        ]);
957        vitem.set_stroke_width(0.04);
958        vitem
959    }
960
961    fn assert_affine_eq(actual: DAffine3, expected: DAffine3) {
962        assert!(
963            actual
964                .transform_point3(dvec3(0.3, -0.7, 1.1))
965                .abs_diff_eq(expected.transform_point3(dvec3(0.3, -0.7, 1.1)), 1e-9)
966        );
967        for i in 0..3 {
968            assert!(
969                actual
970                    .matrix3
971                    .col(i)
972                    .abs_diff_eq(expected.matrix3.col(i), 1e-9),
973                "matrix3 column {i} diverges"
974            );
975        }
976        assert!(actual.translation.abs_diff_eq(expected.translation, 1e-9));
977    }
978
979    #[test]
980    fn nested_extract_composes_matrices_and_keeps_points_local() {
981        // Leaf -> Translation((3,4,5)) -> Similarity(scale 2, t (10,20,30)).
982        // World = S * T: translation becomes (16,28,40).
983        let inner_similarity = Similarity {
984            scale: 2.0,
985            rotation: DQuat::IDENTITY,
986            translation: dvec3(10.0, 20.0, 30.0),
987        };
988        let tree = Transformed::new(
989            CoreNode::new(
990                None,
991                vec![Transformed::new(
992                    CoreNode::new(
993                        Some(CoreVItem {
994                            points: vec![Vec4::new(1.0, 0.0, 0.0, 0.0)],
995                            ..Default::default()
996                        }),
997                        Vec::new(),
998                    ),
999                    DAffine3::from(Translation(dvec3(3.0, 4.0, 5.0))),
1000                )],
1001            ),
1002            DAffine3::from(inner_similarity),
1003        );
1004
1005        let expected_world =
1006            DAffine3::from(inner_similarity) * DAffine3::from(Translation(dvec3(3.0, 4.0, 5.0)));
1007        assert_affine_eq(tree.leaves().next().unwrap().0, expected_world);
1008
1009        match &tree.extract()[0] {
1010            CoreItem::VItem(extracted) => {
1011                // Local data stays byte-identical; the world placement moves.
1012                assert_eq!(extracted.points[0], Vec4::new(1.0, 0.0, 0.0, 0.0));
1013                assert_eq!(
1014                    extracted.transform,
1015                    Mat4::from_scale_rotation_translation(
1016                        Vec3::splat(2.0),
1017                        Quat::IDENTITY,
1018                        Vec3::new(16.0, 28.0, 40.0),
1019                    )
1020                );
1021            }
1022            _ => panic!("expected a VItem"),
1023        }
1024    }
1025
1026    #[test]
1027    fn dfs_emission_matches_painters_order_on_three_levels() {
1028        //        root
1029        //        /   \
1030        //      g1    leaf(2)
1031        //      |
1032        //      g2
1033        //      |
1034        //    leaf(1), leaf(3)
1035        let tree = CoreNode::<DAffine3>::group(vec![
1036            CoreNode::<DAffine3>::group(vec![CoreNode::<DAffine3>::group(vec![
1037                CoreNode::leaf(marked_core_vitem(1.0)),
1038                CoreNode::leaf(marked_core_vitem(3.0)),
1039            ])]),
1040            CoreNode::leaf(marked_core_vitem(2.0)),
1041        ]);
1042
1043        let markers: Vec<f32> = tree
1044            .extract()
1045            .into_iter()
1046            .map(|item| match item {
1047                CoreItem::VItem(vitem) => vitem.points[0].x,
1048                _ => panic!("expected a VItem"),
1049            })
1050            .collect();
1051        // Depth-first: the deeply nested branch paints entirely first.
1052        assert_eq!(markers, [1.0, 3.0, 2.0]);
1053    }
1054
1055    #[test]
1056    fn aabb_follows_node_rotation_and_empty_groups_degenerate() {
1057        // Axis-aligned box x in [-1, 3], y in [-1, 1]: anchors along a
1058        // rectangular loop with colinear midpoint handles.
1059        let rect = HierarchyVItem::from_vpoints(vec![
1060            dvec3(-1.0, -1.0, 0.0),
1061            dvec3(1.0, -1.0, 0.0),
1062            dvec3(3.0, -1.0, 0.0),
1063            dvec3(3.0, 0.0, 0.0),
1064            dvec3(3.0, 1.0, 0.0),
1065            dvec3(1.0, 1.0, 0.0),
1066            dvec3(-1.0, 1.0, 0.0),
1067            dvec3(-1.0, 0.0, 0.0),
1068            dvec3(-1.0, -1.0, 0.0),
1069        ]);
1070        let rotated: Transformed<Node<HierarchyVItem>, DAffine3> =
1071            Node::leaf(rect).transformed(DAffine3::from_rotation_translation(
1072                DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
1073                DVec3::ZERO,
1074            ));
1075
1076        let [lo, hi] = rotated.aabb();
1077        // Rotating (+90deg about Z, x' = -y, y' = x) maps the box onto
1078        // x in [-1, 1], y in [-1, 3].
1079        assert!(lo.abs_diff_eq(dvec3(-1.0, -1.0, 0.0), 1e-9), "lo is {lo:?}");
1080        assert!(hi.abs_diff_eq(dvec3(1.0, 3.0, 0.0), 1e-9), "hi is {hi:?}");
1081
1082        // An empty frame degenerates to the zero box.
1083        let empty = Node::<HierarchyVItem>::frame();
1084        assert_eq!(empty.aabb(), [DVec3::ZERO, DVec3::ZERO]);
1085    }
1086
1087    #[test]
1088    fn centroid_weights_each_flattened_leaf_equally() {
1089        // One side has 3 leaves at x = 3, 4, 5; the other a single leaf at
1090        // x = -36. Equal weighting gives (3+4+5-36)/4 = -6, while per-child
1091        // weighting (subtree average 4 mixed half-and-half) would give -16.
1092        let asymmetric = Node::<DVec3>::group(vec![
1093            Node::group(vec![
1094                Node::leaf(dvec3(3.0, 0.0, 0.0)),
1095                Node::leaf(dvec3(4.0, 0.0, 0.0)),
1096                Node::leaf(dvec3(5.0, 0.0, 0.0)),
1097            ]),
1098            Node::leaf(dvec3(-36.0, 0.0, 0.0)),
1099        ]);
1100
1101        let centroid = Centroid.locate(&asymmetric);
1102        assert!(
1103            centroid.abs_diff_eq(dvec3(-6.0, 0.0, 0.0), 1e-9),
1104            "centroid is {centroid:?}"
1105        );
1106        assert_ne!(centroid, dvec3(-16.0, 0.0, 0.0));
1107
1108        let empty = Node::<DVec3>::frame();
1109        assert_eq!(Centroid.locate(&empty), DVec3::ZERO);
1110    }
1111
1112    #[test]
1113    fn alignment_fills_missing_nodes_with_transparent_clones() {
1114        // Leaf <-> group mismatch: each absent payload/child is filled with
1115        // a transparent clone of the present side.
1116        let left = Node::leaf(stroked_vitem(0.0));
1117        let right = Node::<HierarchyVItem>::group(vec![
1118            Node::leaf(stroked_vitem(2.0)).transformed(DAffine3::from(Translation(DVec3::Y))),
1119        ]);
1120
1121        assert!(!left.is_aligned(&right));
1122        let mut left = left;
1123        let mut right = right;
1124        left.align_with(&mut right);
1125        assert!(left.is_aligned(&right));
1126        assert_eq!(left.children().len(), 1);
1127        assert_eq!(right.children().len(), 1);
1128        assert_eq!(right.item().unwrap().stroke_rgbas[0].0.w, 0.0);
1129        assert_eq!(left.item().unwrap().stroke_rgbas[0].0.w, 1.0);
1130
1131        // Lerping fades the filled positions while the original payload
1132        // stays put and the child pose is static.
1133        let mid = left.lerp(&right, 0.5);
1134        assert_eq!(mid.children().len(), 1);
1135        let mid_item = mid.item().unwrap();
1136        assert!((mid_item.vpoints[0].x - 0.0).abs() < 1e-6);
1137        assert!((mid_item.stroke_rgbas[0].0.w - 0.5).abs() < 1e-6);
1138        let (mid_world, mid_leaf) = mid.leaves().nth(1).unwrap();
1139        assert!((mid_leaf.vpoints[0].x - 2.0).abs() < 1e-6);
1140        assert!((mid_leaf.stroke_rgbas[0].0.w - 0.5).abs() < 1e-6);
1141        assert_affine_eq(mid_world, DAffine3::from_translation(DVec3::Y));
1142
1143        // Different sibling counts: the shorter side is padded with fully
1144        // transparent stand-ins and the originals stay opaque.
1145        let big = Node::<HierarchyVItem>::group(vec![
1146            Node::leaf(stroked_vitem(0.0)),
1147            Node::leaf(stroked_vitem(10.0)),
1148        ]);
1149        let small = Node::<HierarchyVItem>::group(vec![Node::leaf(stroked_vitem(20.0))]);
1150        assert!(!big.is_aligned(&small));
1151        let mut big = big;
1152        let mut small = small;
1153        small.align_with(&mut big);
1154        assert!(big.is_aligned(&small));
1155        assert_eq!(small.children().len(), 2);
1156        let stand_in = small.children()[1].inner.item().unwrap();
1157        assert_eq!(stand_in.stroke_rgbas[0].0.w, 0.0);
1158        assert_eq!(stand_in.fill_rgbas[0].0.w, 0.0);
1159        let original = small.children()[0].inner.item().unwrap();
1160        assert_eq!(original.stroke_rgbas[0].0.w, 1.0);
1161    }
1162
1163    #[test]
1164    fn root_apply_transform_poses_without_baking_points() {
1165        let tree = CoreNode::leaf(CoreVItem {
1166            points: vec![Vec4::new(1.0, 0.0, 0.0, 0.0)],
1167            ..Default::default()
1168        });
1169        let mut tree: Transformed<CoreNode, DAffine3> =
1170            Transformed::new(tree, DAffine3::from(Translation(DVec3::X)));
1171        tree.apply(Rigid::from_translation(DVec3::Y));
1172
1173        match &tree.extract()[0] {
1174            CoreItem::VItem(extracted) => {
1175                // Points are byte-identical, only the matrix updated.
1176                assert_eq!(extracted.points[0], Vec4::new(1.0, 0.0, 0.0, 0.0));
1177                assert_eq!(
1178                    extracted.transform,
1179                    Mat4::from_translation(Vec3::new(1.0, 1.0, 0.0))
1180                );
1181            }
1182            _ => panic!("expected a VItem"),
1183        }
1184    }
1185
1186    #[test]
1187    fn interpolation_moves_poses_only() {
1188        let geometry =
1189            || HierarchyVItem::from_vpoints(vec![dvec3(0.0, 0.0, 0.0), dvec3(1.0, 0.0, 0.0)]);
1190        let start: Transformed<Node<HierarchyVItem>, DAffine3> = Node::leaf(geometry())
1191            .with_id("start")
1192            .transformed(DAffine3::from(Translation(dvec3(1.0, 0.0, 0.0))));
1193        let end: Transformed<Node<HierarchyVItem>, DAffine3> = Node::leaf(geometry())
1194            .with_id("end")
1195            .transformed(DAffine3::from(Translation(dvec3(3.0, 2.0, 0.0))));
1196
1197        assert!(start.is_aligned(&end));
1198        let mid = start.lerp(&end, 0.5);
1199        assert_affine_eq(
1200            mid.transform,
1201            DAffine3::from_translation(dvec3(2.0, 1.0, 0.0)),
1202        );
1203        assert_eq!(mid.inner.id.as_deref(), Some("end"));
1204    }
1205
1206    #[test]
1207    #[should_panic(expected = "aligned")]
1208    fn unaligned_kind_lerp_panics_with_guidance() {
1209        let leaf: Node<u32> = Node::leaf(1);
1210        let group: Node<u32> = Node::group(vec![Node::leaf(2)]);
1211        drop(leaf.lerp(&group, 0.5));
1212    }
1213
1214    #[test]
1215    fn unequal_sibling_counts_follow_vecs_truncating_zip() {
1216        let few = Node::<u32>::group(vec![Node::leaf(1)]);
1217        let many = Node::<u32>::group(vec![Node::leaf(1), Node::leaf(2), Node::leaf(3)]);
1218        let mid = many.lerp(&few, 0.5);
1219        assert_eq!(mid.children().len(), 1);
1220    }
1221
1222    #[test]
1223    fn partial_forwards_ranges_down_recursively() {
1224        let base = stroked_vitem(0.0);
1225        let tree: Transformed<Node<HierarchyVItem>, DAffine3> =
1226            Node::leaf(base.clone()).transformed(DAffine3::from(Translation(DVec3::X)));
1227
1228        let partial = tree.get_partial(0.25..0.75);
1229        assert_eq!(partial.transform, DAffine3::from(Translation(DVec3::X)));
1230        assert_eq!(
1231            partial.inner.item().unwrap(),
1232            &base.get_partial(0.25..0.75),
1233            "the range must be forwarded verbatim"
1234        );
1235
1236        let closed = tree.get_partial_closed(0.25..0.75);
1237        assert_eq!(
1238            closed.inner.item().unwrap(),
1239            &base.get_partial_closed(0.25..0.75)
1240        );
1241
1242        let grouped = Node::<HierarchyVItem>::group(vec![Node::leaf(base.clone()); 3]);
1243        let partial = grouped.get_partial(0.0..0.5);
1244        assert_eq!(partial.children().len(), 3);
1245    }
1246
1247    #[test]
1248    fn empty_is_a_bare_leaf_with_empty_geometry() {
1249        // An unplaced node carries no pose, so `Empty` only fixes the
1250        // structure: a payload of empty geometry, no children.
1251        let empty = Node::<HierarchyVItem>::empty();
1252        assert!(empty.is_leaf());
1253        assert_eq!(empty.children().len(), 0);
1254        let leaf = empty.item().unwrap();
1255        assert_eq!(leaf.stroke_widths[0].0, 0.0);
1256        assert!(leaf.fill_rgbas.iter().all(|rgba| rgba.0 == Vec4::ZERO));
1257
1258        // Placing it defaults to the storage group's identity pose.
1259        let placed: Transformed<Node<HierarchyVItem>, DAffine3> = empty.into();
1260        assert_eq!(placed.transform, DAffine3::IDENTITY);
1261    }
1262
1263    #[test]
1264    fn leaves_accumulate_correctly_over_skewed_affine_chains() {
1265        let scale = DAffine3::from_scale(dvec3(2.0, 3.0, 4.0));
1266        let rotate = DAffine3::from_rotation_translation(
1267            DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
1268            DVec3::ZERO,
1269        );
1270        let translate = DAffine3::from_translation(DVec3::X);
1271
1272        // root -> child -> grandchild -> leaf, each carrying one factor.
1273        let tree = Transformed::new(
1274            CoreNode::new(
1275                None,
1276                vec![Transformed::new(
1277                    CoreNode::new(
1278                        None,
1279                        vec![Transformed::new(
1280                            CoreNode::new(
1281                                Some(CoreVItem {
1282                                    points: vec![Vec4::ZERO],
1283                                    ..Default::default()
1284                                }),
1285                                Vec::new(),
1286                            ),
1287                            translate,
1288                        )],
1289                    ),
1290                    rotate,
1291                )],
1292            ),
1293            scale,
1294        );
1295
1296        let expected = scale * rotate * translate;
1297        let (world, _) = tree.leaves().next().unwrap();
1298
1299        // Hand-check the skewed composition: X is scaled to 2 then rotated
1300        // onto Y and scaled by 3, so translating by X lands at (0, 3, 0).
1301        assert!(
1302            world
1303                .transform_point3(DVec3::ZERO)
1304                .abs_diff_eq(dvec3(0.0, 3.0, 0.0), 1e-9)
1305        );
1306        assert_affine_eq(world, expected);
1307    }
1308
1309    #[test]
1310    fn wrapped_payloads_lift_into_placed_leaf_nodes() {
1311        let similarity = Similarity {
1312            scale: 2.0,
1313            rotation: DQuat::IDENTITY,
1314            translation: dvec3(1.0, 2.0, 3.0),
1315        };
1316        let wrapped = Transformed::new(stroked_vitem(0.0), similarity);
1317
1318        // Lifting a placed payload into a node keeps the pose external.
1319        let placed = wrapped.map_inner(Node::<HierarchyVItem, Similarity>::leaf);
1320        assert!(placed.inner.is_leaf());
1321        assert_eq!(placed.inner.id, None);
1322        assert_eq!(placed.transform, similarity);
1323
1324        // Plain nodes place with the identity pose, so plain and wrapped
1325        // children mix in one group.
1326        let tree = Node::<HierarchyVItem, Rigid>::group(vec![
1327            Node::leaf(stroked_vitem(1.0)).transformed(Rigid::from_translation(DVec3::X)),
1328            Node::leaf(stroked_vitem(2.0)).into(),
1329        ]);
1330        assert_eq!(tree.children[0].transform.translation, DVec3::X);
1331        assert_eq!(tree.children[1].transform, Rigid::IDENTITY);
1332    }
1333
1334    #[test]
1335    fn map_inner_maps_payloads_and_keeps_the_shape() {
1336        let tree = Node::<u32, Translation>::group(vec![
1337            Node::leaf(1)
1338                .with_id("one")
1339                .transformed(Translation(DVec3::X)),
1340            Node::group(vec![Node::leaf(2)]).into(),
1341        ]);
1342        let mapped = tree.map_inner(|payload| payload * 10);
1343
1344        assert_eq!(
1345            mapped.get(&[0]).unwrap().inner.item(),
1346            Some(&10),
1347            "ids and shape survive mapping"
1348        );
1349        assert_eq!(mapped.get(&[0]).unwrap().inner.id.as_deref(), Some("one"));
1350        assert_eq!(mapped.get(&[0]).unwrap().transform, Translation(DVec3::X));
1351        assert_eq!(mapped.get(&[1, 0]).unwrap().inner.item(), Some(&20));
1352    }
1353
1354    #[test]
1355    fn id_and_index_lookups_address_placements() {
1356        // A node's own id addresses its frame, not a placement: the root
1357        // itself cannot be looked up.
1358        let root = Node::<(), DAffine3>::leaf(()).with_id("root");
1359        assert!(root.by_id("root").is_none());
1360        assert_eq!(root.by_id_path("root"), None);
1361
1362        // Index paths walk children and fail closed.
1363        let tree = Node::<u32>::group(vec![
1364            Node::group(vec![Node::leaf(1), Node::leaf(2)]),
1365            Node::leaf(3),
1366        ]);
1367        assert!(tree.get(&[]).is_none());
1368        assert_eq!(tree.get(&[0]).unwrap().inner.children().len(), 2);
1369        assert_eq!(tree.get(&[0, 1]).unwrap().inner.item(), Some(&2));
1370        assert_eq!(tree.get(&[1]).unwrap().inner.item(), Some(&3));
1371        assert!(tree.get(&[2]).is_none());
1372        // Cannot descend into a leaf.
1373        assert!(tree.get(&[1, 0]).is_none());
1374
1375        let mut tree = tree;
1376        let target = tree.get_mut(&[0, 0]).unwrap();
1377        assert_eq!(target.inner.item(), Some(&1));
1378        target.transform = DAffine3::from(Translation(DVec3::X));
1379        assert_eq!(
1380            tree.get(&[0, 0]).unwrap().transform,
1381            DAffine3::from(Translation(DVec3::X))
1382        );
1383
1384        // External labels: duplicates resolve preorder-first, and mutation
1385        // reaches exactly the addressed placement.
1386        let tree = Node::<(), DAffine3>::group(vec![
1387            Node::leaf(()).with_id("dup"),
1388            Node::group(vec![
1389                Node::leaf(()).with_id("dup"),
1390                Node::leaf(()).with_id("other"),
1391            ]),
1392        ]);
1393        assert!(tree.by_id("dup").unwrap().inner.is_leaf());
1394        assert_eq!(tree.by_ids("dup").len(), 2);
1395        assert_eq!(tree.by_id_path("dup"), Some(vec![0]));
1396        assert_eq!(tree.by_id_path("other"), Some(vec![1, 1]));
1397        assert!(tree.by_id("missing").is_none());
1398
1399        let mut tree = tree;
1400        tree.by_id_mut("other").unwrap().inner.id = Some("renamed".into());
1401        assert!(tree.by_id("other").is_none());
1402        assert!(tree.by_id("renamed").is_some());
1403    }
1404
1405    #[test]
1406    fn styling_traits_read_first_leaf_and_write_every_leaf() {
1407        let mut tree = Node::<HierarchyVItem>::group(vec![
1408            Node::leaf(stroked_vitem(0.0)),
1409            Node::group(vec![
1410                Node::leaf(stroked_vitem(5.0)),
1411                Node::leaf(stroked_vitem(9.0)),
1412            ]),
1413        ]);
1414
1415        // Getters report the first leaf encountered in DFS order.
1416        assert_eq!(tree.stroke_width(), 0.04);
1417        tree.set_opacity(0.5);
1418        let opacities: Vec<f32> = tree
1419            .leaves_mut()
1420            .map(|leaf| leaf.stroke_rgbas[0].0.w)
1421            .collect();
1422        assert_eq!(opacities, [0.5, 0.5, 0.5]);
1423
1424        // Stroke functions forward to each leaf independently.
1425        tree.set_stroke_width(1.0);
1426        let widths: Vec<f32> = tree
1427            .leaves_mut()
1428            .map(|leaf| leaf.stroke_widths[0].0)
1429            .collect();
1430        assert_eq!(widths, [1.0, 1.0, 1.0]);
1431
1432        // An empty tree warns and reports neutral values.
1433        let empty = Node::<HierarchyVItem>::frame();
1434        assert_eq!(empty.stroke_width(), 0.0);
1435        assert_eq!(empty.fill_color(), css::WHITE);
1436    }
1437}