Skip to main content

agent/orbs/engine/
mod.rs

1//! Backend-agnostic animation engine (ported from thinking-orbs).
2
3mod braid;
4mod concepts;
5mod core;
6mod lattice;
7mod morph;
8mod orbits;
9mod profiles;
10mod ribbon;
11mod web;
12
13pub use core::{Dot, Frame, Line, Proj, make_proj, sort_dots};
14pub use profiles::{
15    MAX_ICON_D, MAX_MORPH_DOTS, MAX_NODE_N, MAX_SIZE, MIN_SIZE, ModeOpts, base_profile,
16    sanitize_mode_opts, sanitize_size, scale_counts, scale_radii,
17};
18
19use crate::orbs::types::ModeKey;
20use braid::draw_braid_into;
21use concepts::{draw_echo_into, draw_focus_into, draw_gyroscope_into};
22use lattice::{draw_globe_into, draw_rubik_into, draw_wave_into};
23use morph::draw_morph_into;
24use orbits::draw_orbits_into;
25use ribbon::draw_ribbon_into;
26use web::draw_web_into;
27
28/// Paint one frame for the given mode into an existing [`Frame`], reusing its
29/// buffers.
30///
31/// This is the allocation-free entry point for the **geometry** buffers: `out`
32/// is cleared but keeps its capacity, so a steady-state animation loop performs
33/// no heap growth after the first frame (the Solving mode reuses thread-local
34/// scratch for its move tables). Prefer this over [`draw_mode`] in render loops.
35///
36/// `size` and every count-like field of `opts` are sanitized (finite + hard
37/// ceilings) before geometry runs — see [`sanitize_mode_opts`].
38pub fn draw_mode_into(mode: ModeKey, size: f32, t: f32, opts: &ModeOpts, out: &mut Frame) {
39    let size = sanitize_size(size);
40    let opts = sanitize_mode_opts(opts);
41    let t = if t.is_finite() { t } else { 0.0 };
42    draw_mode_into_resolved(mode, size, t, &opts, out);
43}
44
45/// Fast path for the widget's already-sanitized, size-bounded presets.
46///
47/// Keeping this crate-private preserves the safety contract of the public
48/// power-user entry point while avoiding a 40-field clone/sanitize pass on
49/// every widget tick.
50pub(crate) fn draw_mode_into_resolved(
51    mode: ModeKey,
52    size: f32,
53    t: f32,
54    opts: &ModeOpts,
55    out: &mut Frame,
56) {
57    out.clear();
58    match mode {
59        ModeKey::Orbits => draw_orbits_into(size, t, opts, out),
60        ModeKey::Globe => draw_globe_into(size, t, opts, out),
61        ModeKey::Rubik => draw_rubik_into(size, t, opts, out),
62        ModeKey::Wave => draw_wave_into(size, t, opts, out),
63        ModeKey::Web => draw_web_into(size, t, opts, out),
64        ModeKey::Braid => draw_braid_into(size, t, opts, out),
65        ModeKey::Ribbon | ModeKey::Ring => draw_ribbon_into(size, t, opts, out),
66        ModeKey::Morph => draw_morph_into(size, t, opts, out),
67        ModeKey::Focus => draw_focus_into(size, t, opts, out),
68        ModeKey::Gyroscope => draw_gyroscope_into(size, t, opts, out),
69        ModeKey::Echo => draw_echo_into(size, t, opts, out),
70    }
71    // Face-on modes intentionally emit z=0 in painter order, so a comparison
72    // sort cannot change their result. Avoid it entirely.
73    if !matches!(mode, ModeKey::Focus | ModeKey::Echo) {
74        sort_dots(&mut out.dots);
75    }
76}
77
78/// Paint one frame for the given mode into a fresh [`Frame`].
79///
80/// Convenience wrapper around [`draw_mode_into`] for one-off calls and tests;
81/// it allocates. Render loops should use [`draw_mode_into`] instead.
82pub fn draw_mode(mode: ModeKey, size: f32, t: f32, opts: &ModeOpts) -> Frame {
83    let mut frame = Frame::new();
84    draw_mode_into(mode, size, t, opts, &mut frame);
85    frame
86}