neco-edge-routing 0.1.0

necosystems series 2D edge routing primitives for node graphs
Documentation
use alloc::vec;
use alloc::vec::Vec;

use neco_nurbs::NurbsCurve2D;

use crate::{feature_control_points, PathData, PathKind, RouteRequest, RoutingError};

pub(crate) fn route(req: &RouteRequest, degree: u32) -> Result<PathData, RoutingError> {
    let degree = clamp_degree(degree);
    let control_points = feature_control_points(req);
    let points_2d = control_points.map(|(x, y)| [x, y]);
    let knots = clamped_uniform_knots(degree, points_2d.len());
    let curve = NurbsCurve2D::new(degree, points_2d.to_vec(), knots);

    curve.validate().map_err(|_| RoutingError::InvalidInput {
        reason: "nurbs control points, weights, or knots are inconsistent",
    })?;

    Ok(PathData {
        points: curve
            .control_points
            .iter()
            .map(|point| (point[0], point[1]))
            .collect(),
        kind: PathKind::Nurbs {
            knots: curve.knots.clone(),
            weights: curve.weights.clone(),
        },
    })
}

fn clamp_degree(degree: u32) -> usize {
    let requested = usize::try_from(degree).unwrap_or(usize::MAX);
    requested.clamp(1, 3)
}

fn clamped_uniform_knots(degree: usize, control_point_count: usize) -> Vec<f64> {
    let knot_count = control_point_count + degree + 1;
    let interior_count = knot_count.saturating_sub((degree + 1) * 2);
    let mut knots = vec![0.0; degree + 1];
    for index in 1..=interior_count {
        knots.push(index as f64 / (interior_count + 1) as f64);
    }
    knots.extend(vec![1.0; degree + 1]);
    knots
}

#[cfg(test)]
mod tests {
    use super::route;
    use crate::{PathKind, RouteRequest, RouteStyle};

    fn request(degree: u32) -> RouteRequest {
        RouteRequest {
            from: (0.0, 0.0),
            to: (10.0, 4.0),
            from_tangent: (1.0, 0.0),
            to_tangent: (-1.0, 0.0),
            style: RouteStyle::Nurbs { degree },
        }
    }

    #[test]
    fn degree_three_emits_expected_control_data() {
        let path = route(&request(3), 3).expect("nurbs route");
        assert_eq!(path.points.len(), 4);
        match path.kind {
            PathKind::Nurbs { knots, weights } => {
                assert_eq!(knots.len(), 8);
                assert!(weights
                    .iter()
                    .all(|weight| (*weight - 1.0).abs() < f64::EPSILON));
            }
            other => panic!("unexpected kind: {other:?}"),
        }
    }

    #[test]
    fn tuple_array_tuple_conversion_is_lossless() {
        let path = route(&request(3), 3).expect("nurbs route");
        assert_eq!(path.points[0], (0.0, 0.0));
        assert_eq!(path.points[3], (10.0, 4.0));
    }

    #[test]
    fn degree_two_adjusts_knot_count() {
        let path = route(&request(2), 2).expect("quadratic nurbs route");
        match path.kind {
            PathKind::Nurbs { knots, .. } => assert_eq!(knots.len(), 7),
            other => panic!("unexpected kind: {other:?}"),
        }
    }

    #[test]
    fn curve_build_does_not_panic() {
        let path = route(&request(1), 1).expect("linear nurbs route");
        assert!(matches!(path.kind, PathKind::Nurbs { .. }));
    }
}