Skip to main content

yog_cli/design/
advancements.rs

1//! A tab of the game's advancements screen, drawn as the game draws it
2//! (`AdvancementsScreen`, `AdvancementTab`, `AdvancementWidget`,
3//! `TreeNodePosition` of 1.20.1–1.21.1): the tree laid out by the game's
4//! algorithm, the window, the root's background, the connectors, the frames
5//! and icons, and a hovered advancement's title bar and description — in
6//! the game's font, in [`SCREEN`] GUI pixels around the window.
7
8use std::path::Path;
9
10use yog_gfx::draw2d::Draw2D;
11use yog_gfx::{Color, GfxContext, Rect, Vec2};
12
13use super::assets::Resources;
14mod args;
15mod layer;
16
17pub use args::{AdvancementArgs, NodeArgs};
18
19use super::{gfx, images, raster};
20
21/// The window, and its tab's view inside it.
22const WINDOW: (f32, f32) = (252.0, 140.0);
23/// Around the window: room for the backdrop to show and a hover's box to turn.
24const MARGIN: f32 = 8.0;
25/// What's drawn: the window and its margin, as big as the editor makes it.
26const SCREEN: (f32, f32) = (WINDOW.0 + 2.0 * MARGIN, WINDOW.1 + 2.0 * MARGIN);
27const VIEW: Rect = Rect {
28    x: 9.0,
29    y: 18.0,
30    w: 234.0,
31    h: 113.0,
32};
33/// Where the tab's title is written.
34const TITLE_AT: Vec2 = Vec2 { x: 8.0, y: 6.0 };
35/// A tree position's step (column, row) and a widget's frame.
36const STEP: (f32, f32) = (28.0, 27.0);
37const FRAME: f32 = 26.0;
38const FRAME_DX: f32 = 3.0;
39const ICON_AT: Vec2 = Vec2 { x: 8.0, y: 5.0 };
40const ICON: f32 = 16.0;
41/// A background tile's side.
42const TILE: f32 = 16.0;
43/// A connector: where it leaves a frame, how far it runs before turning.
44const CONNECT_MID: f32 = 13.0;
45const CONNECT_OUT: f32 = FRAME + 4.0;
46/// The hover: the bar's sprite width, the widest title, the text's place
47/// and line height, the room it leaves before the screen's edge.
48const BAR_W: f32 = 200.0;
49const TITLE_MAX: f32 = 163.0;
50const BAR_TEXT: Vec2 = Vec2 { x: 32.0, y: 9.0 };
51const TEXT_PAD_LEFT: f32 = 3.0;
52const TEXT_PAD_RIGHT: f32 = 5.0;
53const TEXT_LEFT: f32 = 5.0;
54const LINE_H: f32 = 9.0;
55const TITLE_BAR_MIN: f32 = 29.0;
56const DESCRIPTION_TOP: f32 = 17.0;
57/// The description's first line from the top of a box that opens upward.
58const DESCRIPTION_TOP_UP: f32 = 7.0;
59const DESCRIPTION_PAD: f32 = 32.0;
60const FLIP_ROOM: f32 = 6.0;
61/// How far below or above the target width the game tries a description's
62/// line breaks, and how close is close enough.
63const SPLIT_OFFSETS: [f32; 5] = [0.0, 10.0, -10.0, 25.0, -25.0];
64const SPLIT_GOOD_ENOUGH: f32 = 10.0;
65/// What a hover dims the view with (`AdvancementTab`'s fade at its most).
66const HOVER_FADE: Color = Color(0x4C00_0000);
67/// The screen's dimmed world behind the window (`renderBackground`).
68const BACKDROP: (Color, Color) = (Color(0xC010_1010), Color(0xD010_1010));
69const TITLE_COLOR: Color = Color(0xFF40_4040);
70const WHITE: Color = Color(0xFFFF_FFFF);
71const BLACK: Color = Color(0xFF00_0000);
72/// How much bigger than GUI pixels the picture is drawn.
73const SCALE: u32 = 1;
74
75/// One advancement of the tab, its texts as shown.
76#[derive(Debug, Clone)]
77pub struct Node {
78    pub id: String,
79    pub parent: Option<String>,
80    pub icon: Option<String>,
81    /// `task`, `goal` or `challenge`.
82    pub frame: String,
83    pub title: String,
84    pub description: String,
85    /// Drawn as had (the one being edited).
86    pub obtained: bool,
87}
88
89/// What to draw, its texts as shown.
90#[derive(Debug, Clone)]
91pub struct Args {
92    pub path: String,
93    pub title: String,
94    /// The root's background texture (`minecraft:textures/block/calcite.png` or `minecraft:block/calcite`).
95    pub background: Option<String>,
96    pub nodes: Vec<Node>,
97    /// How far the reader dragged the tree, GUI pixels.
98    pub drag: [f32; 2],
99    /// The advancement under the pointer.
100    pub hover: Option<String>,
101}
102
103/// Where an advancement can be pointed at, in screen GUI pixels.
104#[derive(Debug, Clone, PartialEq, yog_data::Schema)]
105pub struct Spot {
106    pub id: String,
107    pub rect: [f32; 4],
108}
109
110/// The drawn screen.
111#[derive(Debug, Clone, yog_data::Schema)]
112pub struct Rendered {
113    pub image: Option<String>,
114    pub size: [f32; 2],
115    /// The view (where dragging scrolls), and each advancement's spot.
116    pub view: [f32; 4],
117    pub spots: Vec<Spot>,
118    /// The drag the tab accepts (clamped as the game clamps its scroll).
119    pub drag: [f32; 2],
120    /// The view's contents at every scroll, for a tool to slide while the
121    /// tree is dragged (none while one is hovered: no drag starts on one).
122    pub layer: Option<layer::Layer>,
123}
124
125/// The tab laid out: each shown advancement (its node's index, spot, its
126/// parent's spot), and where the window, its view and the scrolled tree are.
127struct Tab {
128    spots: Vec<(usize, Vec2, Option<Vec2>)>,
129    /// The scroll with no drag, and the scrolls it can take, per axis.
130    centre: Vec2,
131    range: [[f32; 2]; 2],
132    window: Vec2,
133    view: Vec2,
134    origin: Vec2,
135}
136
137impl Tab {
138    fn view_rect(&self) -> Rect {
139        Rect::new(self.view.x, self.view.y, VIEW.w, VIEW.h)
140    }
141}
142
143/// Draws the tab.
144pub fn render(args: &Args) -> Rendered {
145    let root = super::mod_root(Path::new(&args.path));
146    images::set_document(root.as_deref());
147    let res = Resources::for_mod(root.as_deref());
148    let (tab, drag) = lay_out(args);
149    let ops = gfx::record(SCREEN.0 as i32, SCREEN.1 as i32, |ctx| {
150        draw(ctx, args, &tab)
151    });
152    let canvas = raster::rasterize(&ops, SCREEN.0 as u32, SCREEN.1 as u32, SCALE, &res);
153    let image = canvas.png_url();
154    let layer = args
155        .hover
156        .is_none()
157        .then(|| layer::render(args, &tab, drag, &res))
158        .flatten();
159    let o = tab.origin;
160    Rendered {
161        image,
162        size: [SCREEN.0, SCREEN.1],
163        view: [tab.view.x, tab.view.y, VIEW.w, VIEW.h],
164        spots: tab
165            .spots
166            .iter()
167            .map(|(i, p, _)| Spot {
168                id: args.nodes[*i].id.clone(),
169                rect: [o.x + p.x, o.y + p.y, FRAME + 1.0, FRAME + 1.0],
170            })
171            .collect(),
172        drag,
173        layer,
174    }
175}
176
177/// Where everything goes: the spots, and the tab's extent and its scroll
178/// (`AdvancementTab.addWidget`, `drawContents`, `scroll`); the drag accepted.
179fn lay_out(args: &Args) -> (Tab, [f32; 2]) {
180    let at = layout(&args.nodes);
181    let place = |id: &str| {
182        at.iter()
183            .find(|(i, _)| i == id)
184            .map(|(_, p)| Vec2::new((p.0 * STEP.0).floor(), (p.1 * STEP.1).floor()))
185    };
186    let spots: Vec<(usize, Vec2, Option<Vec2>)> = args
187        .nodes
188        .iter()
189        .enumerate()
190        .filter_map(|(i, n)| Some((i, place(&n.id)?, n.parent.as_deref().and_then(place))))
191        .collect();
192    let min_x = spots.iter().map(|(_, p, _)| p.x).fold(f32::MAX, f32::min);
193    let max_x = spots
194        .iter()
195        .map(|(_, p, _)| p.x + STEP.0)
196        .fold(f32::MIN, f32::max);
197    let min_y = spots.iter().map(|(_, p, _)| p.y).fold(f32::MAX, f32::min);
198    let max_y = spots
199        .iter()
200        .map(|(_, p, _)| p.y + STEP.1)
201        .fold(f32::MIN, f32::max);
202    let centre = Vec2::new(
203        (VIEW.w / 2.0 - ((max_x + min_x) / 2.0).trunc()).trunc(),
204        ((VIEW.h / 2.0).floor() - ((max_y + min_y) / 2.0).trunc()).trunc(),
205    );
206    // The scrolls an axis takes: any that keeps the view inside the tree
207    // once it's bigger than the view, else none but the centre.
208    let range = |centre: f32, extent: f32, min: f32, view: f32| {
209        if extent - min > view {
210            [-(extent - view), 0.0]
211        } else {
212            [centre, centre]
213        }
214    };
215    let range = [
216        range(centre.x, max_x, min_x, VIEW.w),
217        range(centre.y, max_y, min_y, VIEW.h),
218    ];
219    let axis = |centre: f32, drag: f32, [lo, hi]: [f32; 2]| (centre + drag).clamp(lo, hi) - centre;
220    let drag = [
221        axis(centre.x, args.drag[0], range[0]),
222        axis(centre.y, args.drag[1], range[1]),
223    ];
224    let scroll = Vec2::new((centre.x + drag[0]).floor(), (centre.y + drag[1]).floor());
225    let window = Vec2::new(
226        ((SCREEN.0 - WINDOW.0) / 2.0).floor(),
227        ((SCREEN.1 - WINDOW.1) / 2.0).floor(),
228    );
229    let view = Vec2::new(window.x + VIEW.x, window.y + VIEW.y);
230    let origin = Vec2::new(view.x + scroll.x, view.y + scroll.y);
231    (
232        Tab {
233            spots,
234            centre,
235            range,
236            window,
237            view,
238            origin,
239        },
240        drag,
241    )
242}
243
244/// The screen: the world behind dimmed, the view, the window, the title,
245/// the hover.
246fn draw(ctx: &GfxContext, args: &Args, tab: &Tab) {
247    let d = &ctx.draw2d();
248    // The world behind, dimmed (here: nothing behind).
249    d.rect(Rect::new(0.0, 0.0, SCREEN.0, SCREEN.1), BLACK);
250    d.gradient(
251        Rect::new(0.0, 0.0, SCREEN.0, SCREEN.1),
252        BACKDROP.0,
253        BACKDROP.1,
254    );
255    // The view clips what's inside it (`AdvancementTab.drawContents`'
256    // `enableScissor`); the rasterizer takes the scissor in GUI pixels.
257    let v = tab.view_rect();
258    ctx.set_scissor(v.x as i32, v.y as i32, v.w as i32, v.h as i32);
259    draw_background(d, args, v, tab.origin);
260    draw_tree(d, args, &tab.spots, tab.origin);
261    ctx.clear_scissor();
262    draw_window(d, args, tab);
263    let w = tab.window;
264    // The hover (`AdvancementTab.drawTooltips`, `AdvancementWidget.drawHover`).
265    if let Some((i, p, _)) = args
266        .hover
267        .as_deref()
268        .and_then(|h| tab.spots.iter().find(|(i, _, _)| args.nodes[*i].id == h))
269    {
270        d.rect(tab.view_rect(), HOVER_FADE);
271        hover(d, &args.nodes[*i], tab.origin, *p, w);
272    }
273}
274
275/// The window over the view, and the tab's title.
276fn draw_window(d: &Draw2D, args: &Args, tab: &Tab) {
277    let w = tab.window;
278    d.image(WINDOW_SPRITE, Rect::new(w.x, w.y, WINDOW.0, WINDOW.1));
279    d.text(
280        &args.title,
281        Vec2::new(w.x + TITLE_AT.x, w.y + TITLE_AT.y),
282        TITLE_COLOR,
283        false,
284    );
285}
286
287/// The background over `area`, its tiles laid from the tree's `origin`
288/// (`drawContents`: they move with the tree).
289fn draw_background(d: &Draw2D, args: &Args, area: Rect, origin: Vec2) {
290    let background = args.background.as_deref().map_or_else(
291        || yog_ids::game::MISSING_TEXTURE.to_string(),
292        yog_gfx::image::short_source,
293    );
294    let first = |at: f32, o: f32| o + ((at - o) / TILE).floor() * TILE;
295    let mut y = first(area.y, origin.y);
296    while y < area.y + area.h {
297        let mut x = first(area.x, origin.x);
298        while x < area.x + area.w {
299            d.image(&background, Rect::new(x, y, TILE, TILE));
300            x += TILE;
301        }
302        y += TILE;
303    }
304}
305
306/// The connectors (their shadow first), then each advancement's frame and icon.
307fn draw_tree(d: &Draw2D, args: &Args, spots: &[(usize, Vec2, Option<Vec2>)], o: Vec2) {
308    let fill = |r: Rect, c: Color| d.rect(r, c);
309    for shadow in [true, false] {
310        for (_, p, parent) in spots {
311            if let Some(pp) = parent {
312                connector(&fill, o, *pp, *p, shadow);
313            }
314        }
315    }
316    for (i, p, _) in spots {
317        let n = &args.nodes[*i];
318        let frame = Rect::new(o.x + p.x + FRAME_DX, o.y + p.y, FRAME, FRAME);
319        d.image(&frame_sprite(&n.frame, n.obtained), frame);
320        if let Some(icon) = &n.icon {
321            d.item(
322                icon,
323                Vec2::new(o.x + p.x + ICON_AT.x, o.y + p.y + ICON_AT.y),
324                ICON,
325            );
326        }
327    }
328}
329
330/// The window's sprite and a frame's (`yog-gfx/assets/sprites.ydata`).
331const WINDOW_SPRITE: &str = "sprite:advancement_window";
332fn frame_sprite(frame: &str, obtained: bool) -> String {
333    format!(
334        "sprite:advancement/{frame}{}",
335        if obtained { "_obtained" } else { "" }
336    )
337}
338fn bar_sprite(obtained: bool) -> &'static str {
339    if obtained {
340        "sprite:advancement/box_obtained"
341    } else {
342        "sprite:advancement/box"
343    }
344}
345const DESCRIPTION_SPRITE: &str = "sprite:advancement/description";
346
347/// The frame that's a challenge, and the chat colours `FrameType` writes descriptions in.
348const CHALLENGE: &str = "challenge";
349const DARK_PURPLE: char = '5';
350const GREEN: char = 'a';
351
352/// The colour a frame's description is written in (`FrameType`'s chat colour).
353fn description_color(frame: &str) -> Color {
354    let code = if frame == CHALLENGE {
355        DARK_PURPLE
356    } else {
357        GREEN
358    };
359    Color(super::font::chat_color(code).unwrap_or(Color::WHITE.0))
360}
361
362/// `AdvancementWidget.drawConnectivity`: `GuiGraphics.hLine` / `vLine` rectangles.
363fn connector(fill: &impl Fn(Rect, Color), o: Vec2, parent: Vec2, child: Vec2, shadow: bool) {
364    let (i, j, k) = (
365        o.x + parent.x + CONNECT_MID,
366        o.x + parent.x + CONNECT_OUT,
367        o.y + parent.y + CONNECT_MID,
368    );
369    let (l, m) = (o.x + child.x + CONNECT_MID, o.y + child.y + CONNECT_MID);
370    let h = |a: f32, b: f32, y: f32, c: Color| {
371        fill(Rect::new(a.min(b), y, (a - b).abs() + 1.0, 1.0), c)
372    };
373    let v = |x: f32, a: f32, b: f32, c: Color| {
374        fill(
375            Rect::new(x, a.min(b) + 1.0, 1.0, ((a - b).abs() - 1.0).max(0.0)),
376            c,
377        )
378    };
379    if shadow {
380        h(j, i, k - 1.0, BLACK);
381        h(j + 1.0, i, k, BLACK);
382        h(j, i, k + 1.0, BLACK);
383        h(l, j - 1.0, m - 1.0, BLACK);
384        h(l, j - 1.0, m, BLACK);
385        h(l, j - 1.0, m + 1.0, BLACK);
386        v(j - 1.0, m, k, BLACK);
387        v(j + 1.0, m, k, BLACK);
388    } else {
389        h(j, i, k, WHITE);
390        h(l, j, m, WHITE);
391        v(j, m, k, WHITE);
392    }
393}
394
395/// `AdvancementWidget.drawHover`, an advancement had or not (no progress between).
396fn hover(d: &yog_gfx::draw2d::Draw2D<'_>, n: &Node, o: Vec2, p: Vec2, window: Vec2) {
397    let width_of = |s: &str| d.text_width(s, "");
398    let title = cut_to_width(&n.title, TITLE_MAX, &width_of);
399    let mut inner = TITLE_BAR_MIN + width_of(&title);
400    let lines = optimal_lines(&n.description, inner, &width_of);
401    for l in &lines {
402        inner = inner.max(width_of(l));
403    }
404    let width = inner + TEXT_PAD_LEFT + TEXT_PAD_RIGHT;
405    let y = o.y + p.y;
406    // Flipped left when it would run off the screen; the description above when below has no room.
407    let flip = o.x + p.x + width + FRAME >= SCREEN.0;
408    let up =
409        VIEW.h - (o.y - window.y - VIEW.y) - p.y - FRAME <= FLIP_ROOM + lines.len() as f32 * LINE_H;
410    let x = if flip {
411        o.x + p.x - width + FRAME + FLIP_ROOM
412    } else {
413        o.x + p.x
414    };
415    let box_h = DESCRIPTION_PAD + lines.len() as f32 * LINE_H;
416    if !lines.is_empty() {
417        let top = if up { y + FRAME - box_h } else { y };
418        d.image(DESCRIPTION_SPRITE, Rect::new(x, top, width, box_h));
419    }
420    let half = (width / 2.0).floor();
421    let rest = width - half;
422    d.image_region(
423        bar_sprite(n.obtained),
424        Rect::new(x, y, half, FRAME),
425        Some(Rect::new(0.0, 0.0, half, FRAME)),
426        WHITE,
427    );
428    d.image_region(
429        bar_sprite(n.obtained),
430        Rect::new(x + half, y, rest, FRAME),
431        Some(Rect::new(BAR_W - rest, 0.0, rest, FRAME)),
432        WHITE,
433    );
434    d.image(
435        &frame_sprite(&n.frame, n.obtained),
436        Rect::new(o.x + p.x + FRAME_DX, y, FRAME, FRAME),
437    );
438    let title_x = if flip {
439        x + TEXT_LEFT
440    } else {
441        o.x + p.x + BAR_TEXT.x
442    };
443    d.text(&title, Vec2::new(title_x, y + BAR_TEXT.y), WHITE, true);
444    let color = description_color(&n.frame);
445    for (i, line) in lines.iter().enumerate() {
446        let ly = if up {
447            y + FRAME - box_h + DESCRIPTION_TOP_UP + i as f32 * LINE_H
448        } else {
449            y + BAR_TEXT.y + DESCRIPTION_TOP + i as f32 * LINE_H
450        };
451        d.text(line, Vec2::new(x + TEXT_LEFT, ly), color, false);
452    }
453    if let Some(icon) = &n.icon {
454        d.item(icon, Vec2::new(o.x + p.x + ICON_AT.x, y + ICON_AT.y), ICON);
455    }
456}
457
458/// `Font.substrByWidth`: the longest start of `s` no wider than `max`.
459fn cut_to_width(s: &str, max: f32, width: &impl Fn(&str) -> f32) -> String {
460    let mut end = s.len();
461    while end > 0 && width(&s[..end]) > max {
462        end = s[..end].char_indices().last().map_or(0, |(i, _)| i);
463    }
464    s[..end].to_string()
465}
466
467/// `AdvancementWidget.findOptimalLines`: the breaks whose widest line is
468/// nearest `target`, trying a few widths around it.
469fn optimal_lines(text: &str, target: f32, width: &impl Fn(&str) -> f32) -> Vec<String> {
470    if text.is_empty() {
471        return Vec::new();
472    }
473    let mut best: Option<(f32, Vec<String>)> = None;
474    for off in SPLIT_OFFSETS {
475        let lines = split_lines(text, target - off, width);
476        let gap = (lines.iter().map(|l| width(l)).fold(0.0, f32::max) - target).abs();
477        if gap <= SPLIT_GOOD_ENOUGH {
478            return lines;
479        }
480        if best.as_ref().is_none_or(|(g, _)| gap < *g) {
481            best = Some((gap, lines));
482        }
483    }
484    best.map(|(_, l)| l).unwrap_or_default()
485}
486
487/// `StringSplitter.splitLines`: words onto lines no wider than `max`
488/// (a word wider than a line is broken), `\n` breaking always.
489fn split_lines(text: &str, max: f32, width: &impl Fn(&str) -> f32) -> Vec<String> {
490    let mut out = Vec::new();
491    for para in text.split('\n') {
492        let mut line = String::new();
493        for word in para.split(' ') {
494            let candidate = if line.is_empty() {
495                word.to_string()
496            } else {
497                format!("{line} {word}")
498            };
499            if width(&candidate) <= max || line.is_empty() && width(word) <= max {
500                line = candidate;
501                continue;
502            }
503            if !line.is_empty() {
504                out.push(std::mem::take(&mut line));
505            }
506            let mut rest = word;
507            while width(rest) > max {
508                let cut = cut_to_width(rest, max, width);
509                if cut.is_empty() {
510                    break;
511                }
512                out.push(cut.clone());
513                rest = &rest[cut.len()..];
514            }
515            line = rest.to_string();
516        }
517        out.push(line);
518    }
519    out
520}
521
522/// Each advancement's place: `(column, row)` — the game's `TreeNodePosition`
523/// (Walker's tidy tree), children in the order given; each root's tree
524/// under the one before.
525pub fn layout(nodes: &[Node]) -> Vec<(String, (f32, f32))> {
526    let ids: Vec<&str> = nodes.iter().map(|n| n.id.as_str()).collect();
527    let mut tree = Tree::default();
528    let mut out = Vec::new();
529    let mut offset = 0.0;
530    for root in nodes.iter().filter(|n| parent_in(n, &ids).is_none()) {
531        let r = tree.build(&root.id, None, None, 1, &Source { nodes, ids: &ids });
532        tree.first_walk(r);
533        let top = tree.second_walk(r, 0.0, 0, tree.n[r].y);
534        if top < 0.0 {
535            tree.third_walk(r, -top);
536        }
537        let mut bottom: f32 = 0.0;
538        tree.each(r, &mut |n| {
539            out.push((n.id.clone(), (n.x as f32, n.y + offset)));
540            bottom = bottom.max(n.y);
541        });
542        offset += bottom + 1.0;
543    }
544    // A cycle leaves some unplaced: a column of their own.
545    for n in nodes {
546        if !out.iter().any(|(id, _)| *id == n.id) {
547            out.push((n.id.clone(), (0.0, offset)));
548            offset += 1.0;
549        }
550    }
551    out
552}
553
554/// `n`'s parent among `ids` (none: a root).
555fn parent_in<'a>(n: &'a Node, ids: &[&str]) -> Option<&'a str> {
556    n.parent
557        .as_deref()
558        .filter(|p| ids.contains(p) && *p != n.id)
559}
560
561/// The advancements a tree is built from, and their ids.
562struct Source<'a> {
563    nodes: &'a [Node],
564    ids: &'a [&'a str],
565}
566
567#[derive(Default)]
568struct Tree {
569    n: Vec<TreeNode>,
570}
571
572struct TreeNode {
573    id: String,
574    parent: Option<usize>,
575    previous: Option<usize>,
576    child_index: usize,
577    children: Vec<usize>,
578    ancestor: usize,
579    thread: Option<usize>,
580    x: i32,
581    y: f32,
582    modifier: f32,
583    change: f32,
584    shift: f32,
585}
586
587impl Tree {
588    fn build(
589        &mut self,
590        id: &str,
591        parent: Option<usize>,
592        previous: Option<usize>,
593        child_index: usize,
594        src: &Source,
595    ) -> usize {
596        let me = self.n.len();
597        let x = parent.map_or(0, |p| self.n[p].x + 1);
598        self.n.push(TreeNode {
599            id: id.to_string(),
600            parent,
601            previous,
602            child_index,
603            children: Vec::new(),
604            ancestor: me,
605            thread: None,
606            x,
607            y: -1.0,
608            modifier: 0.0,
609            change: 0.0,
610            shift: 0.0,
611        });
612        let mut prev = None;
613        for child in src
614            .nodes
615            .iter()
616            .filter(|c| parent_in(c, src.ids) == Some(id))
617        {
618            if self.n.iter().any(|t| t.id == child.id) {
619                continue;
620            }
621            let index = self.n[me].children.len() + 1;
622            let c = self.build(&child.id, Some(me), prev, index, src);
623            self.n[me].children.push(c);
624            prev = Some(c);
625        }
626        me
627    }
628
629    fn each(&self, at: usize, f: &mut impl FnMut(&TreeNode)) {
630        f(&self.n[at]);
631        for &c in &self.n[at].children.clone() {
632            self.each(c, f);
633        }
634    }
635
636    fn first_walk(&mut self, v: usize) {
637        let children = self.n[v].children.clone();
638        if children.is_empty() {
639            self.n[v].y = self.n[v].previous.map_or(0.0, |p| self.n[p].y + 1.0);
640            return;
641        }
642        let mut default = None;
643        for &c in &children {
644            self.first_walk(c);
645            default = Some(self.apportion(c, default.unwrap_or(c)));
646        }
647        self.execute_shifts(v);
648        let (Some(&first), Some(&last)) = (children.first(), children.last()) else {
649            return;
650        };
651        let mid = (self.n[first].y + self.n[last].y) / 2.0;
652        match self.n[v].previous {
653            Some(p) => {
654                self.n[v].y = self.n[p].y + 1.0;
655                self.n[v].modifier = self.n[v].y - mid;
656            }
657            None => self.n[v].y = mid,
658        }
659    }
660
661    fn second_walk(&mut self, v: usize, offset: f32, column: i32, mut top: f32) -> f32 {
662        self.n[v].y += offset;
663        self.n[v].x = column;
664        top = top.min(self.n[v].y);
665        let m = self.n[v].modifier;
666        for c in self.n[v].children.clone() {
667            top = self.second_walk(c, offset + m, column + 1, top);
668        }
669        top
670    }
671
672    fn third_walk(&mut self, v: usize, y: f32) {
673        self.n[v].y += y;
674        for c in self.n[v].children.clone() {
675            self.third_walk(c, y);
676        }
677    }
678
679    fn execute_shifts(&mut self, v: usize) {
680        let (mut shift, mut change) = (0.0, 0.0);
681        for &c in self.n[v].children.clone().iter().rev() {
682            self.n[c].y += shift;
683            self.n[c].modifier += shift;
684            change += self.n[c].change;
685            shift += self.n[c].shift + change;
686        }
687    }
688
689    fn previous_or_thread(&self, v: usize) -> Option<usize> {
690        self.n[v]
691            .thread
692            .or_else(|| self.n[v].children.first().copied())
693    }
694
695    fn next_or_thread(&self, v: usize) -> Option<usize> {
696        self.n[v]
697            .thread
698            .or_else(|| self.n[v].children.last().copied())
699    }
700
701    fn apportion(&mut self, v: usize, mut default: usize) -> usize {
702        let (Some(previous), Some(parent)) = (self.n[v].previous, self.n[v].parent) else {
703            return default;
704        };
705        let (mut in_right, mut out_right, mut in_left, mut out_left) =
706            (v, v, previous, self.n[parent].children[0]);
707        let (mut s_in_right, mut s_out_right) = (self.n[v].modifier, self.n[v].modifier);
708        let (mut s_in_left, mut s_out_left) = (self.n[in_left].modifier, self.n[out_left].modifier);
709        while let (Some(nl), Some(pr)) = (
710            self.next_or_thread(in_left),
711            self.previous_or_thread(in_right),
712        ) {
713            in_left = nl;
714            in_right = pr;
715            // The outer contours are as deep as the inner ones.
716            let (Some(ol), Some(or)) = (
717                self.previous_or_thread(out_left),
718                self.next_or_thread(out_right),
719            ) else {
720                break;
721            };
722            (out_left, out_right) = (ol, or);
723            self.n[out_right].ancestor = v;
724            let gap = self.n[in_left].y + s_in_left - (self.n[in_right].y + s_in_right) + 1.0;
725            if gap > 0.0 {
726                let a = self.ancestor_of(in_left, v, default);
727                self.move_subtree(a, v, gap);
728                s_in_right += gap;
729                s_out_right += gap;
730            }
731            s_in_left += self.n[in_left].modifier;
732            s_in_right += self.n[in_right].modifier;
733            s_out_left += self.n[out_left].modifier;
734            s_out_right += self.n[out_right].modifier;
735        }
736        if self.next_or_thread(in_left).is_some() && self.next_or_thread(out_right).is_none() {
737            self.n[out_right].thread = self.next_or_thread(in_left);
738            self.n[out_right].modifier += s_in_left - s_out_right;
739        } else {
740            if self.previous_or_thread(in_right).is_some()
741                && self.previous_or_thread(out_left).is_none()
742            {
743                self.n[out_left].thread = self.previous_or_thread(in_right);
744                self.n[out_left].modifier += s_in_right - s_out_left;
745            }
746            default = v;
747        }
748        default
749    }
750
751    fn ancestor_of(&self, of: usize, v: usize, other: usize) -> usize {
752        let a = self.n[of].ancestor;
753        let siblings = self.n[v].parent.map(|p| &self.n[p].children);
754        if siblings.is_some_and(|s| s.contains(&a)) {
755            a
756        } else {
757            other
758        }
759    }
760
761    fn move_subtree(&mut self, from: usize, to: usize, shift: f32) {
762        let subtrees = self.n[to].child_index as f32 - self.n[from].child_index as f32;
763        if subtrees != 0.0 {
764            self.n[to].change -= shift / subtrees;
765            self.n[from].change += shift / subtrees;
766        }
767        self.n[to].shift += shift;
768        self.n[to].y += shift;
769        self.n[to].modifier += shift;
770    }
771}
772
773#[cfg(test)]
774mod tests;