Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-flow/src/host.rs

//! Caller-owned scenes and transforms. Keep the scene between frames: only setters
//! and graph replacement rebuild derived data, never `paint`.
use crate::{Camera, Graph, scene::Scene};
use egui::{Color32, Pos2, Rect, Stroke, Vec2, vec2};
use std::sync::Arc;

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct View {
    pub scale: f32,
    pub offset: Vec2,
}
impl View {
    /// Navigable's offset is already in screen coordinates; rect is the viewport.
    pub fn from_navigable(nav: &facett_core::nav::Navigable, _rect: Rect) -> Self {
        Self {
            scale: nav.scale,
            offset: vec2(nav.offset[0], nav.offset[1]),
        }
    }
    pub fn screen(&self, p: Pos2) -> Pos2 {
        (p.to_vec2() * self.scale + self.offset).to_pos2()
    }
    pub fn world(&self, p: Pos2) -> Pos2 {
        ((p.to_vec2() - self.offset) / self.scale).to_pos2()
    }
}
impl Camera {
    pub fn view(&self, rect: Rect, bounds: Rect) -> View {
        let fit = ((rect.width() - 64.0).max(1.0) / bounds.width().max(1.0))
            .min((rect.height() - 64.0).max(1.0) / bounds.height().max(1.0));
        let scale = fit * self.zoom;
        View {
            scale,
            offset: rect.center().to_vec2() - vec2(self.centre[0], self.centre[1]) * scale,
        }
    }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CpuStyle {
    Preview,
    Full { max_edges: usize },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Animation {
    Flow,
    External,
    None,
}
#[derive(Clone, Copy, Debug)]
pub struct FlowPalette {
    pub input: Color32,
    pub middle: Color32,
    pub output: Color32,
    pub edge: Color32,
    pub pulse: Color32,
    pub background: Color32,
    #[cfg(feature = "status")]
    pub status: crate::attr::StatusPalette,
}
impl FlowPalette {
    pub fn from_theme(t: &facett_core::Theme) -> Self {
        Self {
            input: t.point,
            middle: t.accent,
            output: t.glow,
            edge: t.edge,
            pulse: t.accent,
            background: t.bg,
            #[cfg(feature = "status")]
            status: crate::attr::StatusPalette::from_theme(t),
        }
    }
    fn node(&self, kind: f32) -> Color32 {
        if kind < 0.5 {
            self.input
        } else if kind > 1.5 {
            self.output
        } else {
            self.middle
        }
    }
}
#[derive(Clone, Debug)]
pub struct PaintOpts {
    pub seconds: f64,
    pub moving: bool,
    pub clustered: bool,
    pub palette: Option<FlowPalette>,
    pub cpu: CpuStyle,
    pub animation: Animation,
    pub label_budget: usize,
    pub min_px: f32,
    /// Hide packets/trails on edges shorter than this many logical screen pixels.
    #[cfg(feature = "lod")]
    pub min_packet_px: f32,
}
impl Default for PaintOpts {
    fn default() -> Self {
        Self {
            seconds: 0.0,
            moving: true,
            clustered: false,
            palette: None,
            cpu: CpuStyle::Preview,
            animation: Animation::Flow,
            label_budget: 2000,
            min_px: 0.0,
            #[cfg(feature = "lod")]
            min_packet_px: 0.0,
        }
    }
}
#[derive(Clone, Debug)]
pub struct OnScreen {
    pub node: usize,
    pub rect: Rect,
    pub px_per_world: f32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Hit {
    Node(usize),
    Edge(usize),
    Group(usize),
}
#[derive(Default)]
pub struct Painted {
    /// Budgeted label candidates, in graph order.
    pub on_screen: Vec<OnScreen>,
    /// All visible node rectangles; never constrained by the label budget.
    pub visible_nodes: Vec<OnScreen>,
    pub truncated: bool,
    pub gpu: bool,
    pub hover: Option<Hit>,
}
#[derive(Clone)]
#[cfg_attr(not(feature = "wgpu"), allow(dead_code))]
pub(crate) struct Attributes {
    pub generation: u64,
    pub nodes: Vec<[f32; 4]>,
    pub edges: Vec<[f32; 4]>,
    pub arrows: bool,
}

#[derive(Clone)]
pub struct FlowScene {
    graph: Graph,
    pub(crate) scene: Scene,
    key: u64,
    digest: u64,
    bounds: Rect,
    node_rects: Vec<Rect>,
    max_half_extent: Vec2,
    #[cfg(feature = "boxes")]
    sizes: Option<crate::attr::Sizes>,
    #[cfg(feature = "status")]
    status: Option<crate::attr::StatusTable>,
    #[cfg(feature = "rate")]
    rates: Option<crate::attr::Rates>,
    #[cfg(feature = "rate")]
    plan: Vec<facett_flowsim::flow::EdgeParam>,
    arrows: bool,
    pub(crate) attributes: Option<Arc<Attributes>>,
    #[cfg(feature = "lod")]
    #[cfg_attr(not(feature = "wgpu"), allow(dead_code))] // neutral resident table is GPU-only
    lod_attributes: std::sync::OnceLock<Arc<Attributes>>,
}
impl FlowScene {
    pub fn new(graph: Graph) -> Result<Self, String> {
        Self::with_key(graph, 0)
    }
    pub fn with_key(graph: Graph, key: u64) -> Result<Self, String> {
        graph.validate()?;
        let digest = crate::scene::digest(&graph, key);
        let mut scene = Scene::new(&graph);
        // Distinct namespace for raw and clustered, including every geometry input.
        Arc::get_mut(&mut scene.raw).unwrap().generation = digest;
        Arc::get_mut(&mut scene.clusters).unwrap().generation =
            digest.rotate_left(29) ^ 0xa52f_6138_be76_d091;
        let bounds = scene.bounds;
        let mut out = Self {
            graph,
            scene,
            key,
            digest,
            bounds,
            node_rects: Vec::new(),
            max_half_extent: Vec2::ZERO,
            arrows: false,
            attributes: None,
            #[cfg(feature = "lod")]
            lod_attributes: std::sync::OnceLock::new(),
            #[cfg(feature = "boxes")]
            sizes: None,
            #[cfg(feature = "status")]
            status: None,
            #[cfg(feature = "rate")]
            rates: None,
            #[cfg(feature = "rate")]
            plan: Vec::new(),
        };
        out.reindex();
        Ok(out)
    }
    #[cfg(feature = "wgpu")]
    fn paint_attributes(&self, _opts: &PaintOpts) -> Option<Arc<Attributes>> {
        #[cfg(feature = "lod")]
        if _opts.min_packet_px > 0.0 && self.attributes.is_none() {
            return Some(
                self.lod_attributes
                    .get_or_init(|| {
                        Arc::new(Attributes {
                            generation: self.digest ^ 0x67ab201d,
                            nodes: vec![[0.0; 4]; self.graph.nodes.len()],
                            edges: vec![[0.18, 1.0, 0.0, 0.0]; self.graph.edges.len()],
                            arrows: false,
                        })
                    })
                    .clone(),
            );
        }
        self.attributes.clone()
    }
    pub fn graph(&self) -> &Graph {
        &self.graph
    }
    pub fn bounds(&self) -> Rect {
        self.bounds
    }
    pub fn generation(&self) -> u64 {
        self.digest
    }
    /// Graph replacement clears all ordering-dependent tables, even at equal length.
    /// An exactly unchanged graph is a no-op, retaining geometry and attributes.
    pub fn replace(&mut self, graph: Graph) -> Result<(), String> {
        if self.graph == graph {
            return Ok(());
        }
        *self = Self::with_key(graph, self.key)?;
        Ok(())
    }
    pub fn node_rect(&self, i: usize) -> Option<Rect> {
        self.node_rects.get(i).copied()
    }
    #[cfg(feature = "status")]
    pub fn status(&self) -> Option<&crate::attr::StatusTable> {
        self.status.as_ref()
    }
    #[cfg(feature = "rate")]
    pub fn rates(&self) -> Option<&crate::attr::Rates> {
        self.rates.as_ref()
    }
    fn visible_nodes(&self, rect: Rect) -> Vec<usize> {
        let query = rect.expand2(self.max_half_extent);
        // Reuse Scene's existing 64-unit node-centre buckets. Its close-view
        // query deliberately refuses huge grids; an overview scans the nodes.
        let cells = ((query.width() / 64.0).ceil() + 2.0) * ((query.height() / 64.0).ceil() + 2.0);
        let mut ids = if cells > 200_000.0 {
            (0..self.node_rects.len()).collect()
        } else {
            self.scene.visible(query, usize::MAX)
        };
        ids.retain(|&i| self.node_rects[i].intersects(rect));
        ids.sort_unstable();
        ids
    }
    fn visible_edges(&self, rect: Rect) -> Vec<usize> {
        (0..self.graph.edges.len())
            .filter(|&i| {
                let (a, b) = self.ends(i);
                Rect::from_two_pos(a, b).intersects(rect)
            })
            .collect()
    }
    pub fn attribute_generation(&self) -> Option<u64> {
        self.attributes.as_ref().map(|a| a.generation)
    }
    #[cfg(feature = "boxes")]
    pub fn set_sizes(&mut self, sizes: crate::attr::Sizes) -> Result<(), String> {
        if sizes.nodes.len() != self.graph.nodes.len()
            || sizes
                .nodes
                .iter()
                .flatten()
                .any(|v| !v.is_finite() || *v <= 0.0 || *v > 2.0e7)
        {
            return Err("Sizes require one finite positive bounded extent per node".into());
        }
        if self.sizes.as_ref() == Some(&sizes) {
            return Ok(());
        }
        self.sizes = Some(sizes);
        self.reindex();
        self.rebuild_attributes();
        Ok(())
    }
    #[cfg(feature = "status")]
    pub fn set_status(&mut self, status: crate::attr::StatusTable) -> Result<(), String> {
        if status.nodes.len() != self.graph.nodes.len()
            || status.edges.len() != self.graph.edges.len()
        {
            return Err("Status table length differs from graph ordering".into());
        }
        if self.status.as_ref() == Some(&status) {
            return Ok(());
        }
        self.status = Some(status);
        self.rebuild_attributes();
        Ok(())
    }
    #[cfg(feature = "rate")]
    pub fn set_rates(&mut self, rates: crate::attr::Rates) -> Result<(), String> {
        if rates.edges.len() != self.graph.edges.len()
            || rates
                .edges
                .iter()
                .flatten()
                .any(|v| !v.is_finite() || *v < 0.0)
        {
            return Err("Rates require one finite nonnegative optional value per edge".into());
        }
        if self.rates.as_ref() == Some(&rates) {
            return Ok(());
        }
        self.plan = facett_flowsim::flow::plan_edges(
            &rates
                .edges
                .iter()
                .map(|v| v.unwrap_or(0.0))
                .collect::<Vec<_>>(),
            &[],
            &facett_flowsim::flow::FlowParams::default(),
        );
        self.rates = Some(rates);
        self.rebuild_attributes();
        Ok(())
    }
    #[cfg(feature = "arrows")]
    pub fn set_arrows(&mut self, value: bool) {
        if self.arrows != value {
            self.arrows = value;
            self.rebuild_attributes();
        }
    }
    fn reindex(&mut self) {
        let rects = self
            .scene
            .raw
            .points
            .iter()
            .enumerate()
            .map(|(_i, p)| {
                #[cfg(feature = "boxes")]
                if let Some(s) = &self.sizes {
                    return Rect::from_center_size(Pos2::from(p.pos), Vec2::from(s.nodes[_i]));
                }
                Rect::from_center_size(Pos2::from(p.pos), Vec2::splat(p.radius * 2.0))
            })
            .collect::<Vec<_>>();
        self.bounds = self.scene.bounds;
        #[cfg(feature = "boxes")]
        if self.sizes.is_some() {
            self.bounds = rects.iter().fold(self.bounds, |r, v| r.union(*v));
        }
        if rects.is_empty() {
            self.bounds = self.scene.bounds;
        }
        self.max_half_extent = rects.iter().fold(Vec2::ZERO, |v, r| v.max(r.size() * 0.5));
        self.node_rects = rects;
    }
    pub(crate) fn ends(&self, i: usize) -> (Pos2, Pos2) {
        let e = &self.scene.raw.links[i];
        let a = Pos2::from(self.scene.raw.points[e.from as usize].pos);
        let b = Pos2::from(self.scene.raw.points[e.to as usize].pos);
        #[cfg(feature = "boxes")]
        if let Some(s) = &self.sizes {
            let vertical = (b.y - a.y).abs() > (b.x - a.x).abs();
            let axis = if vertical {
                vec2(0.0, (b.y - a.y).signum())
            } else {
                vec2((b.x - a.x).signum(), 0.0)
            };
            let k = usize::from(vertical);
            return (
                a + axis * s.nodes[e.from as usize][k] * 0.5,
                b - axis * s.nodes[e.to as usize][k] * 0.5,
            );
        }
        (a, b)
    }
    pub fn edge_point(&self, i: usize, t: f32) -> Option<Pos2> {
        if i >= self.graph.edges.len() || !t.is_finite() {
            return None;
        }
        let (a, b) = self.ends(i);
        Some(crate::curve::point(a, b, t.clamp(0.0, 1.0)))
    }
    pub fn hit(&self, world: Pos2, tol: f32) -> Option<Hit> {
        self.hit_with_candidates(world, tol).0
    }
    /// Candidate count is observable for large-scene performance checks.
    pub fn hit_with_candidates(&self, world: Pos2, tol: f32) -> (Option<Hit>, usize) {
        if !world.x.is_finite() || !world.y.is_finite() || !tol.is_finite() || tol < 0.0 {
            return (None, 0);
        }
        let r = Rect::from_center_size(world, Vec2::splat(tol * 2.0));
        let nodes = self.visible_nodes(r);
        let mut count = nodes.len();
        if let Some(&i) = nodes.last() {
            return (Some(Hit::Node(i)), count);
        }
        for i in self.visible_edges(r) {
            count += 1;
            let (a, b) = self.ends(i);
            let mut p = a;
            for s in 1..=16 {
                let q = crate::curve::point(a, b, s as f32 / 16.0);
                if crate::curve::distance(world, p, q) <= tol {
                    return (Some(Hit::Edge(i)), count);
                }
                p = q;
            }
        }
        (None, count)
    }
    pub fn hits_in(&self, rect: Rect) -> Vec<Hit> {
        let mut hits: Vec<_> = self
            .visible_nodes(rect)
            .into_iter()
            .map(Hit::Node)
            .collect();
        for i in self.visible_edges(rect) {
            let (a, b) = self.ends(i);
            let mut p = a;
            for s in 1..=16 {
                let q = crate::curve::point(a, b, s as f32 / 16.0);
                if segment_intersects(rect, p, q) {
                    hits.push(Hit::Edge(i));
                    break;
                }
                p = q;
            }
        }
        hits
    }
    #[cfg(any(
        feature = "boxes",
        feature = "status",
        feature = "rate",
        feature = "arrows"
    ))]
    #[allow(unused_mut)] // each table is independently feature-gated
    fn rebuild_attributes(&mut self) {
        let mut nodes: Vec<[f32; 4]> = vec![[0.0; 4]; self.graph.nodes.len()];
        let mut edges: Vec<[f32; 4]> = vec![[0.18, 1.0, 0.0, 0.0]; self.graph.edges.len()];
        #[cfg(feature = "boxes")]
        if let Some(s) = &self.sizes {
            for (n, s) in nodes.iter_mut().zip(&s.nodes) {
                n[0] = s[0];
                n[1] = s[1];
            }
        }
        #[cfg(feature = "status")]
        if let Some(s) = &self.status {
            let code = |s: Option<crate::attr::Status>| match s {
                None => 0.0,
                Some(crate::attr::Status::Ok) => 1.0,
                Some(crate::attr::Status::Slow) => 2.0,
                Some(crate::attr::Status::Error) => 3.0,
            };
            for (n, s) in nodes.iter_mut().zip(&s.nodes) {
                n[2] = code(*s);
            }
            for (e, s) in edges.iter_mut().zip(&s.edges) {
                e[2] = code(*s);
            }
        }
        #[cfg(feature = "rate")]
        if let Some(r) = &self.rates {
            for ((e, rate), p) in edges.iter_mut().zip(&r.edges).zip(&self.plan) {
                if rate.is_some() {
                    e[0] = p.speed;
                    e[1] = p.count as f32;
                    e[3] = 1.0;
                }
            }
        }
        if !self.arrows
            && nodes.iter().all(|n| *n == [0.0; 4])
            && edges.iter().all(|e| *e == [0.18, 1.0, 0.0, 0.0])
        {
            self.attributes = None;
            return;
        }
        let mut generation = self.digest;
        for v in nodes.iter().chain(&edges).flatten() {
            crate::scene::hash_bytes(&mut generation, &v.to_bits().to_le_bytes());
        }
        crate::scene::hash_bytes(&mut generation, &[u8::from(self.arrows)]);
        self.attributes = Some(Arc::new(Attributes {
            generation,
            nodes,
            edges,
            arrows: self.arrows,
        }));
    }
}
fn segment_intersects(r: Rect, a: Pos2, b: Pos2) -> bool {
    let d = b - a;
    let mut lo = 0.0_f32;
    let mut hi = 1.0_f32;
    for (origin, delta, min, max) in [(a.x, d.x, r.min.x, r.max.x), (a.y, d.y, r.min.y, r.max.y)] {
        if delta.abs() < 1e-12 {
            if origin < min || origin > max {
                return false;
            }
        } else {
            let x = (min - origin) / delta;
            let y = (max - origin) / delta;
            lo = lo.max(x.min(y));
            hi = hi.min(x.max(y));
            if lo > hi {
                return false;
            }
        }
    }
    true
}

#[allow(unused_mut)] // optional status/rate tables override the plain defaults
pub fn paint(
    ui: &egui::Ui,
    pane: egui::Id,
    rect: Rect,
    scene: &FlowScene,
    view: &View,
    opts: &PaintOpts,
) -> Painted {
    let mut result = Painted::default();
    if !view.scale.is_finite()
        || view.scale <= 0.0
        || !view.offset.x.is_finite()
        || !view.offset.y.is_finite()
        || !rect.is_finite()
    {
        return result;
    }
    let palette = opts
        .palette
        .unwrap_or_else(|| FlowPalette::from_theme(&facett_core::theme(ui)));
    let world = Rect::from_min_max(view.world(rect.min), view.world(rect.max));
    let geometry = if opts.clustered {
        &scene.scene.clusters
    } else {
        &scene.scene.raw
    };
    if !opts.clustered {
        result.visible_nodes = scene
            .visible_nodes(world)
            .into_iter()
            .map(|i| OnScreen {
                node: i,
                rect: Rect::from_min_max(
                    view.screen(scene.node_rects[i].min),
                    view.screen(scene.node_rects[i].max),
                ),
                px_per_world: view.scale,
            })
            .collect();
    } else {
        result.visible_nodes = geometry
            .points
            .iter()
            .enumerate()
            .filter_map(|(i, p)| {
                let r = Rect::from_center_size(
                    view.screen(Pos2::from(p.pos)),
                    Vec2::splat((p.radius * view.scale).clamp(1.2, 14.0) * 2.0),
                );
                r.intersects(rect).then_some(OnScreen {
                    node: i,
                    rect: r,
                    px_per_world: view.scale,
                })
            })
            .collect();
    }
    for n in &result.visible_nodes {
        if n.rect.width().min(n.rect.height()) < opts.min_px {
            continue;
        }
        if result.on_screen.len() == opts.label_budget {
            result.truncated = true;
            break;
        }
        result.on_screen.push(n.clone());
    }
    result.hover = ui
        .input(|i| i.pointer.hover_pos())
        .filter(|p| rect.contains(*p))
        .and_then(|p| {
            if opts.clustered {
                result
                    .visible_nodes
                    .iter()
                    .rev()
                    .find(|n| n.rect.expand(4.0).contains(p))
                    .map(|n| Hit::Group(n.node))
            } else {
                scene.hit(view.world(p), 4.0 / view.scale)
            }
        });
    let moving = opts.moving && opts.animation == Animation::Flow;
    #[cfg(feature = "wgpu")]
    {
        result.gpu = crate::gpu::draw_host(
            ui,
            pane,
            rect,
            geometry.clone(),
            view,
            opts,
            &palette,
            if opts.clustered {
                None
            } else {
                scene.paint_attributes(opts)
            },
        );
    }
    #[cfg(not(feature = "wgpu"))]
    let _ = pane;
    if !result.gpu {
        let painter = ui.painter().with_clip_rect(rect);
        let max_edges = match opts.cpu {
            CpuStyle::Preview => 4000,
            CpuStyle::Full { max_edges } => max_edges,
        };
        let edge_ids = if opts.clustered {
            (0..geometry.links.len()).collect()
        } else {
            scene.visible_edges(world)
        };
        for i in edge_ids.into_iter().take(max_edges) {
            let edge = &geometry.links[i];
            let (a, b) = if opts.clustered {
                (
                    Pos2::from(geometry.points[edge.from as usize].pos),
                    Pos2::from(geometry.points[edge.to as usize].pos),
                )
            } else {
                scene.ends(i)
            };
            let a = view.screen(a);
            let b = view.screen(b);
            let mut col = palette.edge;
            #[cfg(feature = "status")]
            if !opts.clustered
                && let Some(s) = &scene.status
            {
                col = palette.status.color(s.edges[i], col);
            }
            let samples = (0..=16)
                .map(|s| crate::curve::point(a, b, s as f32 / 16.0))
                .collect();
            painter.add(egui::Shape::line(
                samples,
                Stroke::new(1.3, col.gamma_multiply(0.4)),
            ));
            if scene.arrows && !opts.clustered {
                let d = (b - crate::curve::point(a, b, 0.95)).normalized();
                let n = vec2(-d.y, d.x);
                painter.add(egui::Shape::convex_polygon(
                    vec![b, b - d * 8.0 + n * 4.0, b - d * 8.0 - n * 4.0],
                    col,
                    Stroke::NONE,
                ));
            }
            let packets_visible = moving;
            #[cfg(feature = "lod")]
            let packets_visible = packets_visible && a.distance(b) >= opts.min_packet_px;
            if packets_visible {
                let mut speed = 0.18;
                let mut count = 1;
                let mut phase = edge.phase;
                #[cfg(feature = "rate")]
                if !opts.clustered
                    && let Some(r) = &scene.rates
                    && r.edges[i].is_some()
                {
                    speed = scene.plan[i].speed;
                    count = scene.plan[i].count;
                    phase = 0.0;
                }
                for j in 0..count {
                    #[cfg(feature = "rate")]
                    let t =
                        (facett_flowsim::flow::particle_s(j, count, speed, opts.seconds as f32)
                            + phase)
                            .fract();
                    #[cfg(not(feature = "rate"))]
                    let t = (j as f32 / count.max(1) as f32 + speed * opts.seconds as f32 + phase)
                        .fract();
                    painter.circle_filled(crate::curve::point(a, b, t), 2.8, palette.pulse);
                }
            }
        }
        let cap = if opts.cpu == CpuStyle::Preview {
            2000
        } else {
            usize::MAX
        };
        for n in result.visible_nodes.iter().take(cap) {
            let mut col = palette.node(geometry.points[n.node].kind);
            #[cfg(feature = "status")]
            if !opts.clustered
                && let Some(s) = &scene.status
            {
                col = palette.status.color(s.nodes[n.node], col);
            }
            #[cfg(feature = "boxes")]
            if !opts.clustered && scene.sizes.is_some() {
                painter.rect_filled(n.rect, 4.0, col);
                continue;
            }
            if geometry.points[n.node].kind < 2.5 {
                painter.circle_filled(
                    n.rect.center(),
                    (geometry.points[n.node].radius * view.scale).clamp(1.2, 14.0),
                    col,
                );
            }
        }
    }
    result
}

#[cfg(all(test, feature = "rate"))]
mod rate_tests {
    use super::*;
    #[test]
    fn rate_table_uses_flowsim_plan_and_busiest_is_fastest() {
        let mut scene = FlowScene::new(Graph::demo(10)).unwrap();
        let mut values = vec![Some(0.0); scene.graph.edges.len()];
        values[1] = Some(10.0);
        values[2] = Some(100.0);
        scene
            .set_rates(crate::attr::Rates {
                edges: values.clone(),
            })
            .unwrap();
        let expected = facett_flowsim::flow::plan_edges(
            &values.iter().map(|v| v.unwrap()).collect::<Vec<_>>(),
            &[],
            &Default::default(),
        );
        assert_eq!(scene.plan, expected);
        assert_eq!(scene.plan[0].count, 0);
        assert!(scene.plan[2].speed > scene.plan[1].speed);
        let attrs = scene.attributes.as_ref().unwrap();
        assert_eq!(attrs.edges[0][1], 0.0);
        assert_eq!(attrs.edges[2][0], expected[2].speed);
    }
}