path-traits
Tower-like generic traits for parametric paths, segments, and geometric queries.
What is this?
path-traits is a small, dependency-free trait crate that provides a unified interface for parametric curves. It decouples geometric queries (sampling by arc-length, tangents, curvature, projection, composition) from curve representation (line segments, arcs, Béziers, B-splines, NURBS, polylines, and more).
The crate is no_std by default, has zero mandatory dependencies, is #![forbid(unsafe_code)], and is generic over a Scalar type so it works with f32, f64, or custom scalar types via the optional num-traits feature.
Who is it for?
- Path planning / motion planning / robotics libraries that need a shared vocabulary for curve queries.
- Graphics and CAD code that consumes curves generically, regardless of underlying representation.
- Numerical libraries that want arc-length-based sampling, tangents, and curvature without tying to a specific curve type.
- Anyone who wants to write
fn foo<P: Path>(p: &P)once and have it work for every curve type in the ecosystem.
Installation
# Default: no_std, zero deps
# With num-traits for the Float/FloatCore Scalar backend
# With std integrations
# Both
The trait hierarchy at a glance
Scalar,Point,Vector— numeric and geometric primitives.Path,ParametricPath— sample curves by arc-length or normalized parameter.PathSegment,SegmentedPath— work with multi-segment paths like polylines.Tangent,Heading,Curved,FrenetFrame— differential geometry queries.Project— closest-point projection onto a path.PathExt+Reverse,Concat,Offset— composable path adapters.- Free helpers:
equidistant,n_samples,uniform_t. PathError— canonical error enum (everytype Errormust beFrom<PathError<Self::Scalar>>).
Using the traits (consumer guide)
Writing a generic function
use ;
Combining traits for richer queries
use ;
Sampling with helper functions
use ;
// uniform_t requires ParametricPath
use ParametricPath;
Path composition with PathExt
use ;
Closest-point projection
use ;
Implementing the traits (implementer guide)
This section describes what you must implement for each trait, what you get for free, and the invariants your implementation must uphold.
Scalar
The Scalar trait is the numeric backbone of the crate. You typically do not need to implement it — blanket implementations exist for f32 / f64 in all feature configurations. Only implement Scalar manually for exotic numeric types.
Feature-dependent supertraits:
| Features | Supertrait |
|---|---|
| (none) | Add + Sub + Mul + Div + Neg + PartialOrd + Debug + Copy + 'static |
num-traits |
FloatCore + Debug + Copy + 'static |
num-traits + std |
Float + Debug + Copy + 'static |
Required methods: zero(), one(), from_usize(n).
Vector
Vector represents a displacement or derivative in Euclidean space.
Required: zero(), dot(self, rhs), norm(self).
Required operator bounds: Add<Output=Self>, Sub<Output=Self>, Mul<Scalar, Output=Self>.
Invariant: norm() >= 0 and (v * 0).norm() == 0.
Point
Point represents a position in an affine space.
Required: displacement(self, other) -> Vector, translate(self, v) -> Point.
Provided: distance(self, other) (delegates to displacement().norm()).
Invariant: a.translate(a.displacement(b)) == b.
Path
Path is the core trait for arc-length-parameterized curves.
Required associated types:
type Scalar: Scalartype Point: Point<Scalar = Self::Scalar>type Error: From<PathError<Self::Scalar>>
Required methods:
length(&self) -> Self::Scalar— total arc-length.sample_at(&self, s: Self::Scalar) -> Result<Self::Point, Self::Error>— sample at arc-lengths ∈ [0, length].
Provided: start(), end(), domain().
Invariants:
sample_at(0) == start()andsample_at(length()) == end().- Return
PathError::OutOfDomain { param, domain }whens ∉ [0, length]— use thePathError::out_of_domain(s, self.domain())helper. UsePathError::degenerate(reason)for zero-length paths andPathError::not_differentiable(s, reason)for cusps. sample_atshould be arc-length-parameterized (constant speed). If it is not, also implementParametricPathand overridet_to_s/s_to_t.
Error context: PathError<S> carries the offending parameter and valid domain so consumers can produce precise diagnostics:
use ;
ParametricPath
ParametricPath extends Path with normalized-parameter sampling.
Required:
sample_t(&self, t: Self::Scalar) -> Result<Self::Point, Self::Error>— sample att ∈ [0, 1].
Provided (default linear conversion):
t_to_s(&self, t) -> Self::Scalar— default:t * length().s_to_t(&self, s) -> Self::Scalar— default:s / length().
Invariants: sample_t(0) == start(), sample_t(1) == end().
Override t_to_s / s_to_t if your path is not constant-speed.
PathSegment
PathSegment is a marker trait for primitive, non-subdivided curves (a single line segment, a Bézier curve, etc.). Implement it on any type that already implements Path.
SegmentedPath
SegmentedPath is for paths composed of multiple segments (polylines, chains).
Required associated type:
type Segment: PathSegment<Scalar = Self::Scalar, Point = Self::Point, Error = Self::Error>
Note the same-type constraint: Scalar, Point, and Error must all match the parent path.
Required methods:
segment_count(&self) -> usizesegments(&self) -> impl Iterator<Item = &Self::Segment> + '_locate(&self, s: Self::Scalar) -> Result<(usize, Self::Scalar), Self::Error>
Provided: segment(&self, i) (by index, returns Option).
Invariants:
- Segment lengths sum to
length(). locate(s)returnslocal_s ∈ [0, segment_length].
Tangent
Tangent provides the unit tangent vector at any arc-length.
Required:
tangent_at(&self, s) -> Result<Vector, Self::Error>
The returned vector must be unit-length and point in the direction of increasing s. Use PathError::degenerate(reason) for zero-length paths and PathError::not_differentiable(s, reason) for cusps.
Heading
Heading provides the planar heading angle (2D only).
Required:
heading_at(&self, s) -> Result<Self::Scalar, Self::Error>
Returns radians, using the atan2(y, x) convention (counter-clockwise from the positive x-axis).
Curved
Curved provides curvature at any arc-length.
Required associated type:
type Curvature— scalar in 2D, vector in 3D.
Required method:
curvature_at(&self, s) -> Result<Self::Curvature, Self::Error>
Sign convention (2D): positive for left turns (CCW), negative for right turns.
FrenetFrame (advanced)
FrenetFrame provides the full Frenet-Serret frame. This is the most complex differential trait and is optional for basic implementations.
Bounds: Tangent + Curved.
Required associated type:
type Frame— e.g.(Tangent, Normal)in 2D or(T, N, B)in 3D.
Required method:
frame_at(&self, s) -> Result<Self::Frame, Self::Error>
Project
Project provides closest-point projection onto a path.
Required:
project(&self, p: Self::Point) -> Result<Self::Scalar, Self::Error>— returns the arc-lengthsof the closest point.
Provided: closest_point(&self, p) -> Result<Self::Point, Self::Error> (calls project then sample_at).
Invariant: the returned s minimizes path.sample_at(s).distance(p) over [0, length].
Minimal implementation example
Here is a complete, minimal implementation for a 2D line segment:
use *;
// Vector
;
// Point
;
// Path
Adapter bounds note
The .offset() method on PathExt requires Self: Tangent + Heading. If your type does not implement these traits, the compiler will reject .offset() calls. This is intentional — offsetting requires knowledge of the tangent direction and heading to compute the displaced curve.
Feature flags
| Feature | Description |
|---|---|
| (default) | no_std, zero deps. f32/f64 work via manual Scalar impls. |
num-traits |
Uses num-traits as the Scalar backend. Without std, bounded by FloatCore; with std, bounded by Float. |
std |
Enables std-specific integrations. When combined with num-traits, forwards std to that crate. |
The core traits work in all three configurations. f32 and f64 are always available as Scalar types.
MSRV / Edition
Rust 2024 edition, MSRV 1.85+.
License
Licensed under any of:
- EUPL-1.2
- MIT
- Apache-2.0
Choose whichever suits your needs.
Repository
Source code, issues, and pull requests: https://github.com/sunsided/path-traits