Skip to main content

neco_edge_routing/
lib.rs

1#![no_std]
2
3//! necosystems series 2D edge routing primitives for node graphs.
4
5extern crate alloc;
6
7mod bezier;
8mod error;
9mod linear;
10#[cfg(feature = "nurbs")]
11mod nurbs;
12mod orthogonal;
13#[cfg(feature = "spline")]
14mod spline;
15
16use alloc::vec::Vec;
17
18pub use error::RoutingError;
19
20const EPSILON: f64 = 1e-9;
21#[cfg(any(feature = "spline", feature = "nurbs"))]
22const FEATURE_HANDLE_RATIO: f64 = 0.25;
23
24/// Edge routing strategy.
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub enum RouteStyle {
27    /// Direct line segment from `from` to `to`.
28    Linear,
29    /// Cubic Bezier route using tangent-scaled handles.
30    Bezier {
31        /// Multiplier applied to `distance(from, to)` for both handles.
32        curvature: f64,
33    },
34    /// Axis-aligned route with optional rounded corners.
35    Orthogonal {
36        /// Corner radius. Values larger than half the local segment length are clamped.
37        corner_radius: f64,
38    },
39    /// Natural cubic spline route. Requires the `spline` feature.
40    Spline,
41    /// NURBS control path. Requires the `nurbs` feature.
42    Nurbs {
43        /// Requested degree. The implementation clamps this into the valid range.
44        degree: u32,
45    },
46}
47
48/// Input for a routing pass.
49#[derive(Debug, Clone, PartialEq)]
50pub struct RouteRequest {
51    /// Route start point.
52    pub from: (f64, f64),
53    /// Route end point.
54    pub to: (f64, f64),
55    /// Tangent direction that leaves `from`.
56    pub from_tangent: (f64, f64),
57    /// Tangent direction that approaches `to` from the incoming side.
58    pub to_tangent: (f64, f64),
59    /// Requested route style.
60    pub style: RouteStyle,
61}
62
63/// Routed path data.
64#[derive(Debug, Clone, PartialEq)]
65pub struct PathData {
66    /// Control-point layout depends on `kind`.
67    pub points: Vec<(f64, f64)>,
68    /// Semantic interpretation of `points`.
69    pub kind: PathKind,
70}
71
72/// Semantic shape of `PathData.points`.
73#[derive(Debug, Clone, PartialEq)]
74pub enum PathKind {
75    /// Polyline vertices such as `from, bend..., to`.
76    Polyline,
77    /// Cubic segments flattened as `P0, C1, C2, P1` per segment.
78    Cubic,
79    /// Rounded orthogonal path flattened as `from, pre, corner, post, ..., to`.
80    Quadratic,
81    /// NURBS control points plus knot and weight data.
82    Nurbs {
83        /// Knot vector aligned with `points`.
84        knots: Vec<f64>,
85        /// Weight vector aligned with `points`.
86        weights: Vec<f64>,
87    },
88}
89
90/// Compute an edge route without rendering concerns.
91pub fn route(req: &RouteRequest) -> Result<PathData, RoutingError> {
92    validate_request(req)?;
93    match req.style {
94        RouteStyle::Linear => Ok(linear::route(req)),
95        RouteStyle::Bezier { curvature } => Ok(bezier::route(req, curvature)),
96        RouteStyle::Orthogonal { corner_radius } => Ok(orthogonal::route(req, corner_radius)),
97        RouteStyle::Spline => {
98            #[cfg(feature = "spline")]
99            {
100                spline::route(req)
101            }
102            #[cfg(not(feature = "spline"))]
103            {
104                Err(RoutingError::FeatureDisabled { style: "Spline" })
105            }
106        }
107        RouteStyle::Nurbs { degree } => {
108            #[cfg(feature = "nurbs")]
109            {
110                nurbs::route(req, degree)
111            }
112            #[cfg(not(feature = "nurbs"))]
113            {
114                let _ = degree;
115                Err(RoutingError::FeatureDisabled { style: "Nurbs" })
116            }
117        }
118    }
119}
120
121fn validate_request(req: &RouteRequest) -> Result<(), RoutingError> {
122    for value in [
123        req.from.0,
124        req.from.1,
125        req.to.0,
126        req.to.1,
127        req.from_tangent.0,
128        req.from_tangent.1,
129        req.to_tangent.0,
130        req.to_tangent.1,
131    ] {
132        if !value.is_finite() {
133            return Err(RoutingError::InvalidInput {
134                reason: "route request contains non-finite coordinates or tangents",
135            });
136        }
137    }
138
139    match req.style {
140        RouteStyle::Bezier { curvature } if !curvature.is_finite() => {
141            Err(RoutingError::InvalidInput {
142                reason: "bezier curvature must be finite",
143            })
144        }
145        RouteStyle::Orthogonal { corner_radius } if !corner_radius.is_finite() => {
146            Err(RoutingError::InvalidInput {
147                reason: "orthogonal corner radius must be finite",
148            })
149        }
150        _ => Ok(()),
151    }
152}
153
154pub(crate) fn add(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
155    (a.0 + b.0, a.1 + b.1)
156}
157
158pub(crate) fn sub(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
159    (a.0 - b.0, a.1 - b.1)
160}
161
162pub(crate) fn scale(v: (f64, f64), factor: f64) -> (f64, f64) {
163    (v.0 * factor, v.1 * factor)
164}
165
166pub(crate) fn distance(a: (f64, f64), b: (f64, f64)) -> f64 {
167    let d = sub(b, a);
168    sqrt(d.0 * d.0 + d.1 * d.1)
169}
170
171pub(crate) fn length(v: (f64, f64)) -> f64 {
172    sqrt(v.0 * v.0 + v.1 * v.1)
173}
174
175fn sqrt(value: f64) -> f64 {
176    if value <= 0.0 {
177        return 0.0;
178    }
179
180    let mut x = if value >= 1.0 { value } else { 1.0 };
181    for _ in 0..16 {
182        x = 0.5 * (x + value / x);
183    }
184    x
185}
186
187pub(crate) fn is_degenerate_segment(a: (f64, f64), b: (f64, f64)) -> bool {
188    distance(a, b) <= EPSILON
189}
190
191pub(crate) fn is_zero_tangent(v: (f64, f64)) -> bool {
192    length(v) <= EPSILON
193}
194
195pub(crate) fn cubic_control_points(req: &RouteRequest, handle_scale: f64) -> [(f64, f64); 4] {
196    [
197        req.from,
198        add(req.from, scale(req.from_tangent, handle_scale)),
199        add(req.to, scale(req.to_tangent, handle_scale)),
200        req.to,
201    ]
202}
203
204#[cfg(any(feature = "spline", feature = "nurbs"))]
205pub(crate) fn feature_control_points(req: &RouteRequest) -> [(f64, f64); 4] {
206    let handle_scale = distance(req.from, req.to) * FEATURE_HANDLE_RATIO;
207    cubic_control_points(req, handle_scale)
208}