Skip to main content

ranim_items/
hierarchy.rs

1//! Hierarchical scene-graph composition for items.
2//!
3//! This module lifts
4//! [`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`]): each node
9//! stores canonical local data (like
10//! [`Transformed::inner`](ranim_core::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//! - **Render extraction flattens depth-first** and composes placements per node,
16//!   `acc = acc * node_transform`, so the resulting
17//!   render entities preserve
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::transformed::{Transformed, TransformedExt};
27//! use ranim_items::vitem::VItem;
28//! use ranim_core::glam::{DAffine3, dvec3};
29//! use ranim_core::traits::ShiftTransform;
30//! use ranim_core::frame::EvaluatedFrame;
31//! use ranim_items::hierarchy::{Node, PlacedLeaves};
32//!
33//! let stroke = VItem::from_vpoints(vec![dvec3(1.0, 0.0, 0.0)]);
34//! // A tree is placed by wrapping it — root posing is O(1) and never
35//! // bakes points.
36//! let mut tree = Transformed::new(
37//!     Node::<VItem>::group(vec![
38//!         Node::leaf(stroke.clone()).with_id("a"),
39//!         Node::leaf(stroke).with_id("b"),
40//!     ]),
41//!     DAffine3::IDENTITY,
42//! );
43//! tree.shift(dvec3(1.0, 0.0, 0.0));
44//!
45//! let mut frame = EvaluatedFrame::new();
46//! frame.push((0, 0), tree);
47//! let tree = &frame.items::<Transformed<Node<VItem>, DAffine3>>()[0].value;
48//! assert_eq!(tree.leaves().count(), 2);
49//! ```
50
51use std::fmt;
52use std::ops::Range;
53
54use ranim_core::components::width::Width;
55use ranim_core::transformed::Transformed;
56use ranim_core::{
57    anchor::{Aabb, Centroid, Locate},
58    color::{AlphaColor, Srgb, palette::css},
59    glam::{DAffine3, DVec3},
60    traits::{
61        Alignable, Empty, FillColor, Interpolatable, Opacity, Partial, StrokeColor, StrokeWidth,
62        TransformGroup,
63    },
64    utils::resize_preserving_order_with_repeated_indices,
65};
66use tracing::warn;
67
68#[cfg(feature = "render")]
69use ranim_render::extract::{Extract, ExtractContext};
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::transformed::TransformedExt::transformed):
90///
91/// ```
92/// use ranim_core::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 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; render 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`]). Render extraction composes the same placement
325    /// into typed frame entries. 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
532#[cfg(feature = "render")]
533impl<I, G> Extract for Node<I, G>
534where
535    I: Extract,
536    G: Clone + Into<DAffine3>,
537{
538    fn extract(&self, ctx: &mut ExtractContext<'_>) {
539        if let Some(item) = &self.item {
540            item.extract(ctx);
541        }
542        // A node's own payload precedes its placed descendants.
543        for child in &self.children {
544            child.extract(ctx);
545        }
546    }
547}
548
549// MARK: Interpolatable
550
551impl<I, G> Interpolatable for Node<I, G>
552where
553    I: Interpolatable,
554    G: Interpolatable,
555{
556    /// Structural lerp: nodes interpolate positionally, payloads and node
557    /// poses interpolate independently, and ids switch at the mid-point like
558    /// other front-loaded fields in ranim. Callers must have aligned
559    /// structures first (see [`Alignable`]): payload-presence mismatches
560    /// panic, and unequal sibling counts follow `Vec`'s truncating-zip
561    /// precedent.
562    fn lerp(&self, target: &Self, t: f64) -> Self {
563        Self {
564            id: if t < 0.5 {
565                self.id.clone()
566            } else {
567                target.id.clone()
568            },
569            item: lerp_items(&self.item, &target.item, t),
570            children: self
571                .children
572                .iter()
573                .zip(target.children.iter())
574                .map(|(current, target)| current.lerp(target, t))
575                .collect(),
576        }
577    }
578}
579
580/// Structural lerp over payloads. `None` must pair with `None`: a presence
581/// mismatch is filled with transparent clones by [`Alignable`] first.
582fn lerp_items<I: Interpolatable>(current: &Option<I>, target: &Option<I>, t: f64) -> Option<I> {
583    match (current, target) {
584        (Some(current), Some(target)) => Some(current.lerp(target, t)),
585        (None, None) => None,
586        _ => panic!("interpolating unaligned hierarchies: align them with Alignable first"),
587    }
588}
589
590// MARK: Alignable
591
592impl<I, G> Alignable for Node<I, G>
593where
594    I: Alignable + Opacity,
595    G: Clone,
596{
597    /// Whether both sides are already structurally compatible for direct
598    /// interpolation: payload presence must match positionally, sibling
599    /// counts must be equal, and every payload and child pair must satisfy
600    /// [`Alignable::is_aligned`]. This mirrors the `Vec<T>` blanket's
601    /// pre-alignment contract; [`Alignable::align_with`] establishes this
602    /// state from mismatched trees.
603    fn is_aligned(&self, other: &Self) -> bool {
604        let items_aligned = match (&self.item, &other.item) {
605            (Some(current), Some(target)) => current.is_aligned(target),
606            (None, None) => true,
607            _ => false,
608        };
609        items_aligned
610            && self.children.len() == other.children.len()
611            && self
612                .children
613                .iter()
614                .zip(other.children.iter())
615                .all(|(current, target)| current.is_aligned(target))
616    }
617
618    /// Align two trees for interpolation under one uniform rule: **absence
619    /// is filled with a transparent clone of the present side**.
620    ///
621    /// 1. **Payload presence**: when only one side carries an item, the
622    ///    other side receives a transparent (`set_opacity(0.0)`) clone of
623    ///    it, so lerping fades the payload in or out smoothly instead of
624    ///    jumping.
625    /// 2. **Payload pairs**: when both sides carry items, they align with
626    ///    each other (vertex-level padding for point data).
627    /// 3. **Children**: unequal child counts are padded on both sides. A
628    ///    non-empty list grows by repeating its own entries
629    ///    (`resize_preserving_order_with_repeated_indices`, matching the
630    ///    `Vec<T>: Alignable` blanket); an *empty* list has nothing of its
631    ///    own to repeat, so it grows with transparent clones of the other
632    ///    side's children — the same absence rule as payloads. Pairs
633    ///    recurse.
634    fn align_with(&mut self, other: &mut Self) {
635        match (self.item.as_mut(), other.item.as_mut()) {
636            (None, Some(target_item)) => {
637                let mut fill = target_item.clone();
638                fill.set_opacity(0.0);
639                self.item = Some(fill);
640            }
641            (Some(current_item), None) => {
642                let mut fill = current_item.clone();
643                fill.set_opacity(0.0);
644                other.item = Some(fill);
645            }
646            _ => {}
647        }
648        if let (Some(current), Some(target)) = (&mut self.item, &mut other.item) {
649            current.align_with(target);
650        }
651        let len = self.children.len().max(other.children.len());
652        match (self.children.len(), other.children.len()) {
653            (0, 0) => {}
654            (0, _) => self.children = transparent_clones(&other.children, len),
655            (_, 0) => other.children = transparent_clones(&self.children, len),
656            _ => {
657                expand_with_transparent_repeats(&mut self.children, len);
658                expand_with_transparent_repeats(&mut other.children, len);
659            }
660        }
661        self.children
662            .iter_mut()
663            .zip(other.children.iter_mut())
664            .for_each(|(current, target)| current.align_with(target));
665    }
666}
667
668/// Expand `nodes` in place to `len` entries, preserving order; repeated
669/// stand-ins become fully transparent via `set_opacity(0.0)`, matching the
670/// `Vec<T>` align blanket in ranim-core.
671fn expand_with_transparent_repeats<I, G>(nodes: &mut Vec<Transformed<Node<I, G>, G>>, len: usize)
672where
673    I: Opacity + Clone,
674    G: Clone,
675{
676    if nodes.len() != len {
677        let (mut expanded, repeated_idxs) =
678            resize_preserving_order_with_repeated_indices(nodes, len);
679        for idx in repeated_idxs {
680            expanded[idx].set_opacity(0.0);
681        }
682        *nodes = expanded;
683    }
684}
685
686/// Grow `source` to `len` entries by cloning entries and marking every
687/// clone transparent — the absence rule of [`Alignable::align_with`] for
688/// child lists that have nothing of their own to repeat.
689fn transparent_clones<I, G>(
690    source: &[Transformed<Node<I, G>, G>],
691    len: usize,
692) -> Vec<Transformed<Node<I, G>, G>>
693where
694    I: Opacity + Clone,
695    G: Clone,
696{
697    source
698        .iter()
699        .cycle()
700        .take(len)
701        .map(|child| {
702            let mut fill = child.clone();
703            fill.set_opacity(0.0);
704            fill
705        })
706        .collect()
707}
708
709// MARK: Partial
710
711impl<I, G> Partial for Node<I, G>
712where
713    I: Partial,
714    G: Clone,
715{
716    fn get_partial(&self, range: Range<f64>) -> Self {
717        Self {
718            id: self.id.clone(),
719            item: self
720                .item
721                .as_ref()
722                .map(|item| item.get_partial(range.clone())),
723            children: self
724                .children
725                .iter()
726                .map(|child| child.get_partial(range.clone()))
727                .collect(),
728        }
729    }
730
731    fn get_partial_closed(&self, range: Range<f64>) -> Self {
732        Self {
733            id: self.id.clone(),
734            item: self
735                .item
736                .as_ref()
737                .map(|item| item.get_partial_closed(range.clone())),
738            children: self
739                .children
740                .iter()
741                .map(|child| child.get_partial_closed(range.clone()))
742                .collect(),
743        }
744    }
745}
746
747// MARK: Empty
748
749impl<I, G> Empty for Node<I, G>
750where
751    I: Empty,
752{
753    fn empty() -> Self {
754        Node {
755            id: None,
756            // A payload of empty geometry (not `None`), so an `Empty`-seeded
757            // interpolation has a payload position to fade through — parity
758            // with the old leaf-only shape.
759            item: Some(I::empty()),
760            children: Vec::new(),
761        }
762    }
763}
764
765// MARK: Aabb
766
767impl<I, G> Aabb for Node<I, G>
768where
769    I: Aabb,
770    G: Clone + Into<DAffine3>,
771{
772    /// Union of the payload's and the placed children's AABBs — each
773    /// placement's wrapper already applies its own pose, so the result is
774    /// in the receiver's local frame. A node with no payload and no
775    /// children warns and reports a degenerate box, mirroring the slice
776    /// impl in ranim-core.
777    fn aabb(&self) -> [DVec3; 2] {
778        let mut inner_box: Option<[DVec3; 2]> = self.item.as_ref().map(Aabb::aabb);
779        for child in &self.children {
780            let [lo, hi] = child.aabb();
781            inner_box = Some(match inner_box {
782                Some([acc_lo, acc_hi]) => [acc_lo.min(lo), acc_hi.max(hi)],
783                None => [lo, hi],
784            });
785        }
786        inner_box.unwrap_or_else(|| {
787            warn!("Empty bounding box, is the tree empty?");
788            [DVec3::ZERO, DVec3::ZERO]
789        })
790    }
791}
792
793// MARK: Locate
794
795/// The centroid of a tree weights every flattened leaf equally: sum each
796/// leaf's centroid mapped through its accumulated world affine and divide by
797/// the leaf count — NOT per-child weighting. An empty tree warns and returns
798/// zero instead of producing NaNs.
799impl<I, G> Locate<Node<I, G>> for Centroid
800where
801    Centroid: Locate<I>,
802    G: Clone + Into<DAffine3>,
803{
804    fn locate(&self, target: &Node<I, G>) -> DVec3 {
805        let mut sum = DVec3::ZERO;
806        let mut count = 0usize;
807        for (affine, leaf) in target.leaves() {
808            sum += affine.transform_point3(self.locate(leaf));
809            count += 1;
810        }
811        if count == 0 {
812            warn!("Locating the centroid of an empty tree, returning zero");
813            return DVec3::ZERO;
814        }
815        sum / count as f64
816    }
817}
818
819// MARK: Opacity
820
821impl<I, G> Opacity for Node<I, G>
822where
823    I: Opacity,
824{
825    fn set_opacity(&mut self, opacity: f32) -> &mut Self {
826        for leaf in self.leaves_mut() {
827            leaf.set_opacity(opacity);
828        }
829        self
830    }
831}
832
833// MARK: FillColor
834
835impl<I, G> FillColor for Node<I, G>
836where
837    I: FillColor,
838{
839    /// The fill color of the first leaf in DFS order; an empty tree warns
840    /// and reports white.
841    fn fill_color(&self) -> AlphaColor<Srgb> {
842        self.first_leaf()
843            .map(FillColor::fill_color)
844            .unwrap_or_else(|| {
845                warn!("Accessing the fill color of an empty tree, returning white");
846                css::WHITE
847            })
848    }
849
850    fn set_fill_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
851        for leaf in self.leaves_mut() {
852            leaf.set_fill_color(color);
853        }
854        self
855    }
856
857    fn set_fill_opacity(&mut self, opacity: f32) -> &mut Self {
858        for leaf in self.leaves_mut() {
859            leaf.set_fill_opacity(opacity);
860        }
861        self
862    }
863}
864
865// MARK: StrokeColor
866
867impl<I, G> StrokeColor for Node<I, G>
868where
869    I: StrokeColor,
870{
871    /// The stroke color of the first leaf in DFS order; an empty tree warns
872    /// and reports white.
873    fn stroke_color(&self) -> AlphaColor<Srgb> {
874        self.first_leaf()
875            .map(StrokeColor::stroke_color)
876            .unwrap_or_else(|| {
877                warn!("Accessing the stroke color of an empty tree, returning white");
878                css::WHITE
879            })
880    }
881
882    fn set_stroke_color(&mut self, color: AlphaColor<Srgb>) -> &mut Self {
883        for leaf in self.leaves_mut() {
884            leaf.set_stroke_color(color);
885        }
886        self
887    }
888
889    fn set_stroke_opacity(&mut self, opacity: f32) -> &mut Self {
890        for leaf in self.leaves_mut() {
891            leaf.set_stroke_opacity(opacity);
892        }
893        self
894    }
895}
896
897// MARK: StrokeWidth
898
899impl<I, G> StrokeWidth for Node<I, G>
900where
901    I: StrokeWidth,
902{
903    /// The stroke width of the first leaf in DFS order; an empty tree warns
904    /// and reports `0.0`.
905    fn stroke_width(&self) -> f32 {
906        self.first_leaf()
907            .map(StrokeWidth::stroke_width)
908            .unwrap_or_else(|| {
909                warn!("Accessing the stroke width of an empty tree, returning 0");
910                0.0
911            })
912    }
913
914    /// Forward the stroke-width function to every leaf independently.
915    fn apply_stroke_func(&mut self, f: impl for<'a> Fn(&'a mut [Width])) -> &mut Self {
916        for leaf in self.leaves_mut() {
917            leaf.apply_stroke_func(&f);
918        }
919        self
920    }
921}
922
923#[cfg(test)]
924mod tests {
925    use super::*;
926    use crate::hierarchy::PlacedLeaves;
927    use ranim_core::frame::EvaluatedFrame;
928    use ranim_core::glam::{DQuat, Vec4, dvec3};
929    use ranim_core::traits::Translation;
930    use ranim_core::transformed::{Transformed, TransformedExt};
931
932    type VItemNode<G = DAffine3> = Node<HierarchyVItem, G>;
933    type HierarchyVItem = crate::vitem::VItem;
934
935    /// A VItem whose single anchor carries a marker on the x axis, so
936    /// tests can identify each visited leaf.
937    fn marked_vitem(marker: f64) -> HierarchyVItem {
938        HierarchyVItem::from_vpoints(vec![dvec3(marker, 0.0, 0.0)])
939    }
940
941    /// A high-level VItem with opaque strokes, used for opacity inspection.
942    fn stroked_vitem(marker: f64) -> HierarchyVItem {
943        let mut vitem = HierarchyVItem::from_vpoints(vec![
944            dvec3(marker, 0.0, 0.0),
945            dvec3(marker + 0.5, 0.0, 0.0),
946            dvec3(marker + 1.0, 0.0, 0.0),
947        ]);
948        vitem.set_stroke_width(0.04);
949        vitem
950    }
951
952    fn assert_affine_eq(actual: DAffine3, expected: DAffine3) {
953        assert!(actual.matrix3.abs_diff_eq(expected.matrix3, 1e-9));
954        assert!(actual.translation.abs_diff_eq(expected.translation, 1e-9));
955    }
956
957    #[test]
958    fn raw_frame_keeps_nested_hierarchy_placement_and_local_points() {
959        let scale = DAffine3::from_scale(dvec3(2.0, 3.0, 4.0));
960        let rotate = DAffine3::from_rotation_translation(
961            DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
962            DVec3::ZERO,
963        );
964        let translate = DAffine3::from_translation(DVec3::X);
965        let tree = VItemNode::group(vec![
966            VItemNode::group(vec![
967                VItemNode::leaf(marked_vitem(1.0)).transformed(translate),
968            ])
969            .transformed(rotate),
970        ])
971        .transformed(scale);
972        let (world, _) = tree.leaves().next().unwrap();
973        assert_affine_eq(world, scale * rotate * translate);
974        assert!(
975            world
976                .transform_point3(DVec3::ZERO)
977                .abs_diff_eq(dvec3(0.0, 3.0, 0.0), 1e-9)
978        );
979        let mut frame = EvaluatedFrame::new();
980        frame.push((0, 0), tree);
981        let stored = &frame.items::<Transformed<VItemNode, DAffine3>>()[0].value;
982        let (placement, item) = stored.leaves().next().unwrap();
983        assert_eq!(item.vpoints[0], DVec3::X);
984        assert_affine_eq(placement, scale * rotate * translate);
985        assert!(
986            placement
987                .transform_point3(DVec3::X)
988                .abs_diff_eq(dvec3(0.0, 6.0, 0.0), 1e-9)
989        );
990    }
991
992    #[test]
993    fn stored_hierarchy_retains_painters_order_on_three_levels() {
994        //        root
995        //        /   \
996        //      g1    leaf(2)
997        //      |
998        //      g2
999        //      |
1000        //    leaf(1), leaf(3)
1001        let tree = VItemNode::<DAffine3>::group(vec![
1002            VItemNode::<DAffine3>::group(vec![VItemNode::<DAffine3>::group(vec![
1003                VItemNode::leaf(marked_vitem(1.0)),
1004                VItemNode::leaf(marked_vitem(3.0)),
1005            ])]),
1006            VItemNode::leaf(marked_vitem(2.0)),
1007        ]);
1008
1009        let mut frame = EvaluatedFrame::new();
1010        frame.push((0, 0), tree);
1011        let markers: Vec<f64> = frame.items::<VItemNode>()[0]
1012            .value
1013            .leaves()
1014            .map(|(_, item)| item.vpoints[0].x)
1015            .collect();
1016        // Depth-first: the deeply nested branch paints entirely first.
1017        assert_eq!(markers, [1.0, 3.0, 2.0]);
1018    }
1019
1020    #[cfg(feature = "render")]
1021    #[test]
1022    fn render_extraction_preserves_order_pose_and_removes_missing_leaves() {
1023        use crate::vitem::render::RenderVItem;
1024        use bevy_app::SubApp;
1025        use bevy_ecs::{prelude::*, schedule::ScheduleLabel};
1026        use ranim_core::glam::DMat4;
1027        use ranim_render::{extract::ExtractPlugin, schedule::RenderSchedule, world::SourceOrder};
1028
1029        type Input = Transformed<VItemNode, DAffine3>;
1030
1031        let outer = DAffine3::from_scale(dvec3(2.0, 3.0, 1.0));
1032        let first_pose =
1033            DAffine3::from_translation(DVec3::X) * DAffine3::from_translation(DVec3::Z);
1034        let second_pose = DAffine3::from_translation(DVec3::Y);
1035        let first = marked_vitem(1.0);
1036        let input = VItemNode::group(vec![
1037            VItemNode::leaf(first.clone()).transformed(first_pose),
1038            VItemNode::leaf(marked_vitem(2.0)).transformed(second_pose),
1039        ])
1040        .transformed(outer);
1041        let mut frame = EvaluatedFrame::new();
1042        frame.push((7, 0), input);
1043
1044        let mut app = SubApp::new();
1045        app.update_schedule = Some(RenderSchedule::Extract.intern());
1046        app.insert_resource(frame)
1047            .add_plugins(ExtractPlugin::<Input>::default());
1048        app.update();
1049
1050        let world = app.world_mut();
1051        let mut outputs = world
1052            .query::<(Entity, &SourceOrder, &RenderVItem)>()
1053            .iter(world)
1054            .map(|(entity, order, item)| (entity, *order, item.clone()))
1055            .collect::<Vec<_>>();
1056        outputs.sort_by_key(|(_, order, _)| *order);
1057        assert_eq!(outputs.len(), 2);
1058        assert_eq!(outputs[0].1, SourceOrder { order: 0, part: 0 });
1059        assert_eq!(outputs[1].1, SourceOrder { order: 0, part: 1 });
1060        assert_eq!(outputs[0].2.points, RenderVItem::from(first.clone()).points);
1061        assert_eq!(
1062            outputs[0].2.transform,
1063            DMat4::from(outer * first_pose).as_mat4()
1064        );
1065        assert_eq!(
1066            outputs[1].2.transform,
1067            DMat4::from(outer * second_pose).as_mat4()
1068        );
1069        let retained = outputs[0].0;
1070        let removed = outputs[1].0;
1071
1072        let smaller = VItemNode::group(vec![VItemNode::leaf(first).transformed(first_pose)])
1073            .transformed(outer);
1074        world
1075            .resource_mut::<EvaluatedFrame>()
1076            .update([((7, 0), smaller)]);
1077        app.update();
1078        assert!(app.world().entities().contains(retained));
1079        assert!(!app.world().entities().contains(removed));
1080    }
1081
1082    #[test]
1083    fn aabb_follows_node_rotation_and_empty_groups_degenerate() {
1084        // Axis-aligned box x in [-1, 3], y in [-1, 1]: anchors along a
1085        // rectangular loop with colinear midpoint handles.
1086        let rect = HierarchyVItem::from_vpoints(vec![
1087            dvec3(-1.0, -1.0, 0.0),
1088            dvec3(1.0, -1.0, 0.0),
1089            dvec3(3.0, -1.0, 0.0),
1090            dvec3(3.0, 0.0, 0.0),
1091            dvec3(3.0, 1.0, 0.0),
1092            dvec3(1.0, 1.0, 0.0),
1093            dvec3(-1.0, 1.0, 0.0),
1094            dvec3(-1.0, 0.0, 0.0),
1095            dvec3(-1.0, -1.0, 0.0),
1096        ]);
1097        let rotated: Transformed<Node<HierarchyVItem>, DAffine3> =
1098            Node::leaf(rect).transformed(DAffine3::from_rotation_translation(
1099                DQuat::from_rotation_z(std::f64::consts::FRAC_PI_2),
1100                DVec3::ZERO,
1101            ));
1102
1103        let [lo, hi] = rotated.aabb();
1104        // Rotating (+90deg about Z, x' = -y, y' = x) maps the box onto
1105        // x in [-1, 1], y in [-1, 3].
1106        assert!(lo.abs_diff_eq(dvec3(-1.0, -1.0, 0.0), 1e-9), "lo is {lo:?}");
1107        assert!(hi.abs_diff_eq(dvec3(1.0, 3.0, 0.0), 1e-9), "hi is {hi:?}");
1108
1109        // An empty frame degenerates to the zero box.
1110        let empty = Node::<HierarchyVItem>::frame();
1111        assert_eq!(empty.aabb(), [DVec3::ZERO, DVec3::ZERO]);
1112    }
1113
1114    #[test]
1115    fn centroid_weights_each_flattened_leaf_equally() {
1116        // One side has 3 leaves at x = 3, 4, 5; the other a single leaf at
1117        // x = -36. Equal weighting gives (3+4+5-36)/4 = -6, while per-child
1118        // weighting (subtree average 4 mixed half-and-half) would give -16.
1119        let asymmetric = Node::<DVec3>::group(vec![
1120            Node::group(vec![
1121                Node::leaf(dvec3(3.0, 0.0, 0.0)),
1122                Node::leaf(dvec3(4.0, 0.0, 0.0)),
1123                Node::leaf(dvec3(5.0, 0.0, 0.0)),
1124            ]),
1125            Node::leaf(dvec3(-36.0, 0.0, 0.0)),
1126        ]);
1127
1128        let centroid = Centroid.locate(&asymmetric);
1129        assert!(
1130            centroid.abs_diff_eq(dvec3(-6.0, 0.0, 0.0), 1e-9),
1131            "centroid is {centroid:?}"
1132        );
1133
1134        let empty = Node::<DVec3>::frame();
1135        assert_eq!(Centroid.locate(&empty), DVec3::ZERO);
1136    }
1137
1138    #[test]
1139    fn alignment_fills_missing_nodes_with_transparent_clones() {
1140        // Leaf <-> group mismatch: each absent payload/child is filled with
1141        // a transparent clone of the present side.
1142        let left = Node::leaf(stroked_vitem(0.0));
1143        let right = Node::<HierarchyVItem>::group(vec![
1144            Node::leaf(stroked_vitem(2.0)).transformed(DAffine3::from(Translation(DVec3::Y))),
1145        ]);
1146
1147        assert!(!left.is_aligned(&right));
1148        let mut left = left;
1149        let mut right = right;
1150        left.align_with(&mut right);
1151        assert!(left.is_aligned(&right));
1152        assert_eq!(left.children().len(), 1);
1153        assert_eq!(right.children().len(), 1);
1154        assert_eq!(right.item().unwrap().stroke_rgbas[0].0.w, 0.0);
1155        assert_eq!(left.item().unwrap().stroke_rgbas[0].0.w, 1.0);
1156
1157        // Lerping fades the filled positions while the original payload
1158        // stays put and the child pose is static.
1159        let mid = left.lerp(&right, 0.5);
1160        assert_eq!(mid.children().len(), 1);
1161        let mid_item = mid.item().unwrap();
1162        assert!((mid_item.vpoints[0].x - 0.0).abs() < 1e-6);
1163        assert!((mid_item.stroke_rgbas[0].0.w - 0.5).abs() < 1e-6);
1164        let (mid_world, mid_leaf) = mid.leaves().nth(1).unwrap();
1165        assert!((mid_leaf.vpoints[0].x - 2.0).abs() < 1e-6);
1166        assert!((mid_leaf.stroke_rgbas[0].0.w - 0.5).abs() < 1e-6);
1167        assert_affine_eq(mid_world, DAffine3::from_translation(DVec3::Y));
1168
1169        // Different sibling counts: the shorter side is padded with fully
1170        // transparent stand-ins and the originals stay opaque.
1171        let big = Node::<HierarchyVItem>::group(vec![
1172            Node::leaf(stroked_vitem(0.0)),
1173            Node::leaf(stroked_vitem(10.0)),
1174        ]);
1175        let small = Node::<HierarchyVItem>::group(vec![Node::leaf(stroked_vitem(20.0))]);
1176        assert!(!big.is_aligned(&small));
1177        let mut big = big;
1178        let mut small = small;
1179        small.align_with(&mut big);
1180        assert!(big.is_aligned(&small));
1181        assert_eq!(small.children().len(), 2);
1182        let stand_in = small.children()[1].inner.item().unwrap();
1183        assert_eq!(stand_in.stroke_rgbas[0].0.w, 0.0);
1184        assert_eq!(stand_in.fill_rgbas[0].0.w, 0.0);
1185        let original = small.children()[0].inner.item().unwrap();
1186        assert_eq!(original.stroke_rgbas[0].0.w, 1.0);
1187    }
1188
1189    #[test]
1190    fn interpolation_moves_poses_only() {
1191        let geometry =
1192            || HierarchyVItem::from_vpoints(vec![dvec3(0.0, 0.0, 0.0), dvec3(1.0, 0.0, 0.0)]);
1193        let start: Transformed<Node<HierarchyVItem>, DAffine3> = Node::leaf(geometry())
1194            .with_id("start")
1195            .transformed(DAffine3::from(Translation(dvec3(1.0, 0.0, 0.0))));
1196        let end: Transformed<Node<HierarchyVItem>, DAffine3> = Node::leaf(geometry())
1197            .with_id("end")
1198            .transformed(DAffine3::from(Translation(dvec3(3.0, 2.0, 0.0))));
1199
1200        assert!(start.is_aligned(&end));
1201        let mid = start.lerp(&end, 0.5);
1202        assert_affine_eq(
1203            mid.transform,
1204            DAffine3::from_translation(dvec3(2.0, 1.0, 0.0)),
1205        );
1206        assert_eq!(mid.inner.id.as_deref(), Some("end"));
1207    }
1208
1209    #[test]
1210    #[should_panic(expected = "aligned")]
1211    fn unaligned_kind_lerp_panics_with_guidance() {
1212        let leaf: Node<u32> = Node::leaf(1);
1213        let group: Node<u32> = Node::group(vec![Node::leaf(2)]);
1214        drop(leaf.lerp(&group, 0.5));
1215    }
1216
1217    #[test]
1218    fn unequal_sibling_counts_follow_vecs_truncating_zip() {
1219        let few = Node::<u32>::group(vec![Node::leaf(1)]);
1220        let many = Node::<u32>::group(vec![Node::leaf(1), Node::leaf(2), Node::leaf(3)]);
1221        let mid = many.lerp(&few, 0.5);
1222        assert_eq!(mid.children().len(), 1);
1223    }
1224
1225    #[test]
1226    fn partial_forwards_ranges_down_recursively() {
1227        let base = stroked_vitem(0.0);
1228        let tree = Node::<HierarchyVItem>::group(vec![Node::leaf(base.clone()); 2])
1229            .transformed(DAffine3::from_translation(DVec3::X));
1230        for (partial, expected) in [
1231            (tree.get_partial(0.25..0.75), base.get_partial(0.25..0.75)),
1232            (
1233                tree.get_partial_closed(0.25..0.75),
1234                base.get_partial_closed(0.25..0.75),
1235            ),
1236        ] {
1237            assert_eq!(partial.transform, tree.transform);
1238            assert_eq!(
1239                partial
1240                    .inner
1241                    .leaves()
1242                    .map(|(_, leaf)| leaf)
1243                    .collect::<Vec<_>>(),
1244                [&expected; 2]
1245            );
1246        }
1247    }
1248
1249    #[test]
1250    fn empty_is_a_bare_leaf_with_empty_geometry() {
1251        // An unplaced node carries no pose, so `Empty` only fixes the
1252        // structure: a payload of empty geometry, no children.
1253        let empty = Node::<HierarchyVItem>::empty();
1254        assert!(empty.is_leaf());
1255        assert_eq!(empty.children().len(), 0);
1256        let leaf = empty.item().unwrap();
1257        assert_eq!(leaf.stroke_widths[0].0, 0.0);
1258        assert!(leaf.fill_rgbas.iter().all(|rgba| rgba.0 == Vec4::ZERO));
1259
1260        // Placing it defaults to the storage group's identity pose.
1261        let placed: Transformed<Node<HierarchyVItem>, DAffine3> = empty.into();
1262        assert_eq!(placed.transform, DAffine3::IDENTITY);
1263    }
1264
1265    #[test]
1266    fn map_inner_maps_payloads_and_keeps_the_shape() {
1267        let tree = Node::<u32, Translation>::group(vec![
1268            Node::leaf(1)
1269                .with_id("one")
1270                .transformed(Translation(DVec3::X)),
1271            Node::group(vec![Node::leaf(2)]).into(),
1272        ]);
1273        let mapped = tree.map_inner(|payload| payload * 10);
1274
1275        assert_eq!(
1276            mapped.get(&[0]).unwrap().inner.item(),
1277            Some(&10),
1278            "ids and shape survive mapping"
1279        );
1280        assert_eq!(mapped.get(&[0]).unwrap().inner.id.as_deref(), Some("one"));
1281        assert_eq!(mapped.get(&[0]).unwrap().transform, Translation(DVec3::X));
1282        assert_eq!(mapped.get(&[1, 0]).unwrap().inner.item(), Some(&20));
1283    }
1284
1285    #[test]
1286    fn id_and_index_lookups_address_placements() {
1287        // A node's own id addresses its frame, not a placement: the root
1288        // itself cannot be looked up.
1289        let root = Node::<(), DAffine3>::leaf(()).with_id("root");
1290        assert!(root.by_id("root").is_none());
1291        assert_eq!(root.by_id_path("root"), None);
1292
1293        // Index paths walk children and fail closed.
1294        let tree = Node::<u32>::group(vec![
1295            Node::group(vec![Node::leaf(1), Node::leaf(2)]),
1296            Node::leaf(3),
1297        ]);
1298        assert!(tree.get(&[]).is_none());
1299        assert_eq!(tree.get(&[0]).unwrap().inner.children().len(), 2);
1300        assert_eq!(tree.get(&[0, 1]).unwrap().inner.item(), Some(&2));
1301        assert_eq!(tree.get(&[1]).unwrap().inner.item(), Some(&3));
1302        assert!(tree.get(&[2]).is_none());
1303        // Cannot descend into a leaf.
1304        assert!(tree.get(&[1, 0]).is_none());
1305
1306        let mut tree = tree;
1307        let target = tree.get_mut(&[0, 0]).unwrap();
1308        assert_eq!(target.inner.item(), Some(&1));
1309        target.transform = DAffine3::from(Translation(DVec3::X));
1310        assert_eq!(
1311            tree.get(&[0, 0]).unwrap().transform,
1312            DAffine3::from(Translation(DVec3::X))
1313        );
1314
1315        // External labels: duplicates resolve preorder-first, and mutation
1316        // reaches exactly the addressed placement.
1317        let tree = Node::<(), DAffine3>::group(vec![
1318            Node::leaf(()).with_id("dup"),
1319            Node::group(vec![
1320                Node::leaf(()).with_id("dup"),
1321                Node::leaf(()).with_id("other"),
1322            ]),
1323        ]);
1324        assert!(tree.by_id("dup").unwrap().inner.is_leaf());
1325        assert_eq!(tree.by_ids("dup").len(), 2);
1326        assert_eq!(tree.by_id_path("dup"), Some(vec![0]));
1327        assert_eq!(tree.by_id_path("other"), Some(vec![1, 1]));
1328        assert!(tree.by_id("missing").is_none());
1329
1330        let mut tree = tree;
1331        tree.by_id_mut("other").unwrap().inner.id = Some("renamed".into());
1332        assert!(tree.by_id("other").is_none());
1333        assert!(tree.by_id("renamed").is_some());
1334    }
1335
1336    #[test]
1337    fn styling_traits_read_first_leaf_and_write_every_leaf() {
1338        let mut tree = Node::<HierarchyVItem>::group(vec![
1339            Node::leaf(stroked_vitem(0.0)),
1340            Node::group(vec![
1341                Node::leaf(stroked_vitem(5.0)),
1342                Node::leaf(stroked_vitem(9.0)),
1343            ]),
1344        ]);
1345
1346        // Getters report the first leaf encountered in DFS order.
1347        assert_eq!(tree.stroke_width(), 0.04);
1348        tree.set_opacity(0.5);
1349        let opacities: Vec<f32> = tree
1350            .leaves_mut()
1351            .map(|leaf| leaf.stroke_rgbas[0].0.w)
1352            .collect();
1353        assert_eq!(opacities, [0.5, 0.5, 0.5]);
1354
1355        // Stroke functions forward to each leaf independently.
1356        tree.set_stroke_width(1.0);
1357        let widths: Vec<f32> = tree
1358            .leaves_mut()
1359            .map(|leaf| leaf.stroke_widths[0].0)
1360            .collect();
1361        assert_eq!(widths, [1.0, 1.0, 1.0]);
1362
1363        // An empty tree warns and reports neutral values.
1364        let empty = Node::<HierarchyVItem>::frame();
1365        assert_eq!(empty.stroke_width(), 0.0);
1366        assert_eq!(empty.fill_color(), css::WHITE);
1367    }
1368}