sm::bezcurve
A general-order Bezier curve
import sm.bezcurve;
Module file: sm/bezcurve.cppm. Test and example code: examples/bezcurve tests/bez1 tests/bezcurves tests/bezfit tests/bezsplit tests/twocurves tests/bezmatrix
Table of Contents
- Summary
- Creating a curve
- Evaluating the curve
- Fitting a curve to points
- Splitting a curve
- Scaling the output
- String output
Summary
sm::bezcurve<F, order> represents a single Bezier curve of a fixed polynomial degree, order (1 for a line, 2 for a quadratic, 3 for a cubic, and so on) - order has no default value and must always be given explicitly. Its order + 1 control points are stored as an sm::mat<F, order + 1, 2> (one row per control point, columns for x and y) rather than as an array of sm::vecs.
The class implements Cohen & Riesenfeld’s (1982) general matrix representation of Bezier curves: it precomputes a basis-conversion matrix M and caches M * C (C being the control-point matrix) so that evaluating a point at parameter t is a single small matrix multiply, valid for any order. It also contains an implementation of the classical direct Bernstein-polynomial summation as a cross-check. There’s a compile-time ceiling on order of around 19 (tied to a Pascal’s-triangle lookup table sized for orders up to 20) - plenty for any practical use.
sm::bezcurve isn’t constexpr-capable (it uses std::cout, std::format, exceptions, and - for one curve-fitting overload - an internal sm::nm_simplex optimization).
Creating a curve
The most general constructor takes all order + 1 control points (including both endpoints) as an sm::vvec:
sm::vvec<sm::vec<float,2>> c = { {1,1}, {2,8}, {9,8}, {10,1} };
sm::bezcurve<float, 3> cv (c); // order 3 -> needs exactly 4 points
You can also pass the control points directly as an sm::mat<F, order+1, 2>, or use one of the fixed-order convenience constructors:
sm::vec<float,2> ip = {1,1}, fp = {10,1}, c1 = {5,5}, c2 = {2,-4};
sm::bezcurve<float, 3> cubic (ip, fp, c1, c2); // order == 3 only: initial point, final point, 2 control points
sm::bezcurve<float, 2> quad (ip, fp, c1); // order == 2 only: initial point, final point, 1 control point
sm::bezcurve<float, 1> line (ip, fp); // order == 1 only: just the two endpoints
Or, for any order, give the endpoints separately from the interior control points:
sm::vvec<sm::vec<float,2>> interior = { c1, c2 }; // order-1 interior points
sm::bezcurve<float, 3> cv (ip, fp, interior);
All the point-count-based constructors throw std::runtime_error if you supply the wrong number of points for the curve’s order.
To replace all of a curve’s control points after construction, use update_controls (also throws on a size mismatch):
cv.update_controls (c);
Evaluating the curve
compute_point(t) evaluates the curve at parameter t in [0, 1], throwing std::runtime_error outside that range:
sm::bezcoord<float> pt = cv.compute_point (0.4f);
Internally, this selects different methods based on the order; closed-form evaluation for first to third order curves, or the general matrix-multiply method for order >= 4.
compute_point(t, l) starts at parameter t and moves a further Euclidean distance l along the curve, via a binary search over t (the search’s tolerance is set as a percentage of l with set_lthresh(F), default 1). If there isn’t l worth of curve left after t, or the search doesn’t converge, it returns a null bezcoord (is_null() == true) whose remaining field holds the actual distance left. This convention lets sm::bezcurvepath stitch samples smoothly across a sequence of curves.
For sampling many points at once:
std::vector<sm::bezcoord<float>> pts = cv.compute_points (40u); // 40 points, evenly spaced in t
std::vector<sm::bezcoord<float>> pts_l = cv.compute_points (1.0f); // points spaced by Euclidean arc length 1.0
std::vector<sm::bezcoord<float>> pts_x = cv.compute_points_horz (1.0f); // points spaced by horizontal (x) distance
For the arc-length and horizontal-distance overloads, the last element of the returned vector is a null bezcoord carrying whatever distance was left over past the last full step - again, the mechanism bezcurvepath relies on.
compute_tangent_normal(t) returns a std::pair of unit bezcoords, {tangent, normal}:
auto [tangent, normal] = cv.compute_tangent_normal (0.4f);
For order > 1 this is built from derivative<F>(), which returns the order control points of a curve one degree lower (the derivative curve).
Fitting a curve to points
cv.fit (points); // points.size() must equal order + 1, exactly
This is an exact interpolation through the given points (parameterized by their estimated arc-length positions along the curve), not a least-squares fit over more points than the curve has degrees of freedom - fit throws std::runtime_error if points.size() != order + 1. Internally, the fit is computed in double precision even when F is float, because it was found that single precision only gives reliable fits up to around order 4 or 5.
sm::vvec<sm::vec<float,2>> c = { {-0.28f,0.0f}, {0.28f,0.0f}, {0.28f,0.45f}, {-0.28f,0.45f} };
sm::bezcurve<float, 3> cv;
cv.fit (c);
std::cout << cv.get_order() << std::endl; // 3
There’s also a three-argument overload, fit (points, preceding, optimize = false), which additionally smooths the tangent direction across the join with a preceding curve, and - if optimize is true - refines the interior control points with an sm::nm_simplex search that minimizes compute_objective(points).
compute_objective(points) is itself public: it returns the sum of squared distances between arc-length-sampled points on the curve and points (or F{-1} - a sentinel, not an exception - if the sizes don’t line up), and is the quantity the optimizing fit overload minimizes.
Splitting a curve
You can split a curve into two curves at any point t in the range [0, 1]. The function split returns two new control point matrices for the two shorter curves:
auto [c1, c2] = cv.split (0.5f); // control-point matrices of the two halves, split at t=0.5
// explicitly: std::pair<sm::mat<float, 4, 2>, sm::mat<float, 4, 2>> splitpair = cv.split (0.5f);
sm::bezcurve<float, 3> cv1 (c1);
sm::bezcurve<float, 3> cv2 (c2);
Scaling the output
bezcurve has a scaling factor for the output coordinates. This was implemented to apply scaling factors from a SVG file, where curves are defined along with a scale to change into a desired set of units (such cm or mm). The scale defaults to 1, but may be changed wtih set_scale.
cv.set_scale (2.0f); // see caveat below
float len_x2 = cv.get_initial_point_scaled().length(); // scale applied
std::uint32_t ord = cv.get_order();
sm::vvec<sm::vec<float,2>> ctrls = cv.get_controls(); // control points are unscaled
String output
Output the curve
Output functions return a string containing a newline-separated list of coordinates on the curve. You can pass either an unsigned integer argument num_points to output a fixed number of points spaced evenly on the t parameter line, or a floating point argument step to obtain points spaced by a fixed arc length.
std::cout << cv.output (40u); // 40 evenly-spaced-in-t sample points
std::cout << cv.output (0.1f); // sample points spaced by arc-length step 0.1
These produce output like:
6.48923,6.1053
7.40207,5.69326
8.18007,5.07463
...
Output the control matrix
The control matrix is formatted by sm::mat:
std::cout << cv.output_control();
Gives:
| -0.28 ~0 |
| 0.533498 -0.663498 |
| 0.533498 1.1135 |
| -0.28 0.45 |
This page was authored with AI, based on human written code in bezcurve.cppm. Reviewed by Seb James