From 258a0a3e2b276c3e08ec3f6a1b4089b0008efa57 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sun, 22 Feb 2026 10:16:33 -0500 Subject: [PATCH] allow smooth_path to work on length 2 paths with method="corners" --- nurbs.scad | 40 +++++++++++++++++++++++++------------- rounding.scad | 54 +++++++++++++++++++++++++++------------------------ 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/nurbs.scad b/nurbs.scad index 22d835ff..4cc63f54 100644 --- a/nurbs.scad +++ b/nurbs.scad @@ -28,29 +28,41 @@ _BOSL2_NURBS = is_undef(_BOSL2_STD) && (is_undef(BOSL2_NO_STD_WARNING) || !BOSL2 // pts = nurbs_curve(control, degree, splinesteps, [mult=], [weights=], [type=], [knots=]); // pts = nurbs_curve(control, degree, u=, [mult=], [weights=], [type=], [knots=]); // Description: -// Compute the points specified by a NURBS curve. You specify the NURBS by supplying the control points, knots and weights. and knots. +// Compute the points specified by a NURBS curve. You specify the NURBS by supplying the control points, knots and weights. // Only the control points are required. The knots and weights default to uniform, in which case you get a uniform B-spline. +// The length of `weights`, if given, must match the length of `control`. // You can specify endpoint behavior using the `type` parameter. The default, "clamped", gives a curve which starts and // ends at the first and last control points and moves in the tangent direction to the first and last control point segments. -// If you request an "open" spline you get a curve which starts somewhere in the middle of the control points. -// Finally, a "closed" curve is a one that starts where it ends. Note that each of these types of curve require -// a different number of knots. +// A "closed" curve is a one that starts where it ends. An "open" spline is a generic curve that starts somewhere +// in the middle of the control points. The "open" curve is less common; you only need this if you are managing the +// knots and control points yourself to create your own clamped or closed curve, so avoid this type unless you know what you're doing. +// Each of these types of curve require a different number of knots as described below. // . // The control points are the most important control over the shape -// of the curve. You must have at least p+1 control points for clamped and open NURBS. Unlike a bezier, there is no maximum +// of the curve. You must have at least degree+1 control points for clamped and open NURBS. Don't confuse the degree of a +// NURBS with its *order*: the order of a NURBS, often called $p$, is degree+1. Unlike a bezier, there is no maximum // number of control points. A single NURBS is more like a bezier **path** than like a single bezier spline. // . // A NURBS or B-spline is a curve made from a moving average of several Bezier curves. The knots specify when one Bezier fades -// away to be replaced by the next one. At generic points, the curves are differentiable, but by increasing knot multiplicity, you -// can decrease smoothness, or even produce a sharp corner. The knots must be an ascending sequence of values, but repeating values -// is OK and controls the smoothness at the knots. The easiest way to specify the knots is to take the default of uniform knots, -// and simply set the multiplicity to create repeated knots as needed. The total number of knots is then the sum of the multiplicity -// vector. Alternatively you can simply list the knots yourself. Note that regardless of knot values, the domain of evaluation +// away to be replaced by the next one. The knot list is a non-increasing list of values that you specify using two parameters, +// `knots` and `mult`. In practice changing the knot values doesn't have a strong effect on the curve, so it usually suffices +// to use a uniform knot vector, which is the default. The major exception to this is repeated knot values. +// At generic points in the NURBS, the curve is infinitely differentiable, but at a point that +// corresponds to a knot, a NURBS with degree $d$ will have a $(d-1)$th derivative that is continuous. +// However, if a value repeats in the knot vector that creates a knot with a multiplicity larger than 1, and each +// repetition decreases the smoothness of the curve at the corresponding NURBS point by 1. This means that +// if the multiplicity equals the degree then the curve has a corner at the knot point. Using the `mult` parameter +// without giving `knots` allows you to give a vector of multiplicities, which produces a knot vector that is uniform +// except it has some repeated knots. A value of 1 in the `mult` vector means the knot is not repeated; a value of 2 means it is +// repeated twice. The multiplicity can be as large as the degree but no larger. (A special exception is at the ends for open +// NURBS.) When you specify the multiplicity vector the total number of knots is the sum of that vector. You can also list +// the knots explicitly yourself. The knot values you give can cover any range; they will be scaled to correspond properly +// to the NURBS parameter space: rgardless of the knot values you give, the domain of evaluation // for u is always the interval [0,1], and it will be scaled to give the entire valid portion of the curve you have chosen. -// If you give both a knot vector and multiplicity then the multiplicity vector is appled to the provided knots. -// For an open spline the number of knots must be `len(control)+p+1`. For a clamped spline the number of knots is `len(control)-p+1`, +// . +// For an open spline the number of knots must be `len(control)+degree+1`. For a clamped spline the number of knots is `len(control)-degree+1`, // and for a closed spline you need `len(control)+1` knots. If you are using the default uniform knots then the way to -// ensure that you have the right number is to check that `sum(mult)` is either not set or equal to the correct value. +// ensure that you have the right number is to check that mult is not set or `sum(mult)` equals the correct value. // . // You can use this function to evaluate the NURBS at `u`, which can be a single point or a list of points. You can also // use it to evaluate the NURBS over its entire domain by giving a splinesteps value. This specifies the number of segments @@ -236,7 +248,7 @@ function nurbs_curve(control,degree,splinesteps,u, mult,weights,type="clamped", : uniform ? [for(i=idx(mult)) each repeat(i/(len(mult)-1),mult[i])] : let( xknots = is_undef(mult)? knots - : assert(len(mult) == len(knots), "If knot vector and mult vector must be the same length") + : assert(len(mult) == len(knots), "Knot vector and mult vector must be the same length") [for(i=idx(mult)) each repeat(knots[i], mult[i])] ) type=="open" ? assert(len(xknots)==len(control)+degree+1, str("For open spline, knot vector with multiplicity must have length ", diff --git a/rounding.scad b/rounding.scad index ebd4e849..4635a5c8 100644 --- a/rounding.scad +++ b/rounding.scad @@ -612,7 +612,7 @@ function _rounding_offsets(edgespec,z_dir=1) = // For `method="edges"` (default), every segment (edge) of the path is replaced by a cubic curve with `splinesteps` // points, and the cubic interpolation passes through every input point on the path, matching the tangents at every // point. If you do not specify `tangents`, they are computed using {{path_tangents()}} with `uniform=false` by -// default. Only the dirction of a tangent vector matters, not the vector length. +// default. Only the direction of a tangent vector matters, not the vector length. // Setting `uniform=true` with non-uniform sampling may be desirable in some cases but tends to // produces curves that overshoot the point on the path. // . @@ -621,17 +621,21 @@ function _rounding_offsets(edgespec,z_dir=1) = // are tangent to the midpoint of every segment. The `tangents` and `uniform` parameters don't apply to the // "corners" method. Using either one with "corners" causes an error. // . -// The `size` or `relsize` parameters apply to both methods. They determine how far the curve can bend away -// from the input path. In the case where the path has three non-collinear points, the size specifies the -// exact distance between the specified path and the curve (maximum distance from edge if for the "edges" -// method, or distance from corner with the "corners" method). -// In 2D when the spline may make an S-curve, for the "edges" method the size parameter specifies the sum -// of the deviations of the two peaks of the curve. In 3-space the bezier curve may have three extrema: two -// maxima and one minimum. In this case the size specifies the sum of the maxima minus the minimum. +// If the input is collinear, including the case of a 2 point path, then the output is the same as the input +// for the "corners" method and for the "edges" method the output is also the same as the input unless you specify +// tangents that are not aligned with the input path. // . -// If you give `relsize` instead, then for the "edges" method, the maximum deviation from the segment is -// relative to the segment length (e.g. 0.05 means 5% of the segment length). For the "corners" method, -// `relsize` determines where the curve intersects the corner bisector, relative to the maximum deviation +// The `size` and `relsize` parameters apply to both methods. They determine how far the curve can deviate +// from the input path. With the "edges" method each segment is replaced by a bezier. Three things can happen. +// The bezier may bulge to one side or the other of the input segemnt. In this case `size` specifies the exact (maximum) distance +// between the path segment and the output curve. Another possibility is that the spline makes an S-curve. +// In this case, `size` specifies the sum of the deviations at the two peaks of the curve. In 3-space the bezier +// curve may have three extrema: two maxima and one minimum. In this case `size` specifies the sum of the maxima minus the minimum. +// If you give `relsize` instead then `size` is set for each segment of the path relative to the length of that segment, +// so if a segment has length 10 then `relsize=0.05` would choose `size=0.5` for that segment. +// . +// For the "corners" method, `size` specifies the maximum distance from the corner of the original path to the bezier that replaces the corner, +// and `relsize` determines where the curve intersects the corner bisector, relative to the maximum deviation // possible (which corresponds to a circle rounding from the shortest leg of the corner). For example, // `relsize=1` is the maximum deviation from the corner (a circle arc from the shortest leg), and `relsize=0.5` // causes the curve to intersect the corner bisector halfway between that maximum and the tip of the corner. @@ -737,21 +741,21 @@ function _rounding_offsets(edgespec,z_dir=1) = // color("red")move_copies(pts)circle(r=.15,$fn=12); module smooth_path(path, tangents, size, relsize, method="edges", splinesteps=10, uniform, closed=false) {no_module();} function smooth_path(path, tangents, size, relsize, method="edges", splinesteps=10, uniform, closed) = -is_1region(path) - ? smooth_path(path[0], tangents, size, relsize, method, splinesteps, uniform, default(closed,true)) - : assert(method=="edges" || method=="corners", "method must be \"edges\" or \"corners\".") -assert(method=="edges" || (is_undef(tangents) && is_undef(uniform)), "The tangents and uniform parameters are incompatible with method=\"corners\".") -let ( - uniform = default(uniform,false), - bez = method=="edges" - ? path_to_bezpath(path, tangents=tangents, size=size, relsize=relsize, uniform=uniform, closed=default(closed,false)) - : path_to_bezcornerpath(path, size=size, relsize=relsize, closed=default(closed,false)), - smoothed = bezpath_curve(bez,splinesteps=splinesteps) -) -closed ? list_unwrap(smoothed) : smoothed; - - + is_1region(path) + ? smooth_path(path[0], tangents, size, relsize, method, splinesteps, uniform, default(closed,true)) + : assert(method=="edges" || method=="corners", "method must be \"edges\" or \"corners\".") + assert(method=="edges" || (is_undef(tangents) && is_undef(uniform)), "The tangents and uniform parameters are incompatible with method=\"corners\".") + method=="corners" && len(path)==2 ? path + : let( + uniform = default(uniform,false), + bez = method=="edges" + ? path_to_bezpath(path, tangents=tangents, size=size, relsize=relsize, uniform=uniform, closed=default(closed,false)) + : path_to_bezcornerpath(path, size=size, relsize=relsize, closed=default(closed,false)), + smoothed = bezpath_curve(bez,splinesteps=splinesteps) + ) + closed ? list_unwrap(smoothed) : smoothed; + function _scalar_to_vector(value,length,varname) = is_vector(value) ? assert(len(value)==length, str(varname," must be length ",length))