I haven't had the time to understand this yet, but as I understand the discussion around it, this is a way to provide corners with continuous rate of curvature change, making for prettier corners.
use<lib/BOSL/math.scad>
use<lib/BOSL/beziers.scad>
// TODO:
// remove colinear points?
//
// roundcorners(path, curve, type, all, closed)
//
// path: a list of 2d or 3d points, possibly with an extra coordinate giving smoothing parameters, e.g.
// ex: [[0,0],[0,1],[1,1],[0,1]] 2d point list
// [[0,0,0], [0,1,1], [1,1,2], [0,1,3]] 3d point list
// [[0,0,.2],[0,1,.1],[1,1,0],[0,1,.3]] 2d point list with smoothing parameters
// [[0,0,0,.2], [0,1,1,.1], [1,1,2,0], [0,1,3,.3]] 3d point list with smooth parameters
// [[0,0,[.3,.5], [4,0,[.2,.6]], [4,4,0], [0,4,[
// curve: set to "smooth" to get continuous curvature 4th order bezier curves
// set to "circle" to get circular arcs
// type: set to "cut" with either curve type to specify how much of the corner to "cut" off. The
// smoothing parameter will be the distance from the corner to the tip of the rounded curve
// set to "radius" to use with curve=="circle" to specify the curve radius
// set to "joint" to use with curve=="smooth" to specify the distance from the corner at which
// the curve joints the shape
// all: set this to a curvature parameter to apply to all points on the list. If this is set then all
// of the values given in the path are assumed to be geometrical coordinates. If you don't set it
// then the last value of each entry in path is assumed to be the smoothing parameters
// closed: set to true (the default) if the curve is closed and false if the curve is open at the ends
//
// If you select curve=="smooth" then there are two smoothing parameters. The first one
// is the cut or joint distance as given type "type". The second one is a curvature
// parameter which is a number in [0,1], where larger numbers have a more abrupt
// transition and smaller ones a more gradual transition. If the curvature parameter is
// close to zero the transition is so gradual that it may take a very large distance.
//
// If you select curves that are too large to fit the code will fail with an error. It displays a set
// of scale factors that you can apply to the (first) smoothing parameter which will reduce the size of the
// curves so that they will fit on the shape. If the scale factors are larger than one then they
// indicate how much you can increase the curve size before collisions will occur.
//
// https://hackernoon.com/apples-icons-have-that-shape-for-a-very-good-reason-720d4e7c8a14
function roundcorners(path, curve, type, all=undef, closed=true) =
let(
default_curvature = 0.7, // default curvature for "smooth" curves
typeok = type == "cut" || (curve=="circle" && type=="radius") ||
(curve=="smooth" && type=="joint"),
pathdim = array_dim(path,1),
have_all = all==undef ? 1 : 0,
pathsize_ok = is_num(pathdim) && pathdim >= 2+have_all && pathdim <= 3+have_all
)
assert(curve=="smooth" || curve=="circle", "Unknown 'curve' setting in roundcorners")
assert(typeok, curve=="circle" ? "In roundcorners curve==\"circle\" requires 'type' of 'radius' or 'cut'":
"In roundcorners curve==\"smooth\" requires 'type' of 'joint' or 'cut'")
assert(pathdim!=undef, "Input 'path' has entries with inconsistent length")
assert(pathsize_ok, str("Input 'path' must have entries with length ",2+have_all," or ",
3+have_all, all==undef ? " when 'all' is not specified" : "when 'all' is specified"))
let(
pathfixed= all == undef ? path : array_zip([path, replist([all],len(path))]),
dim = len(pathfixed[0])-1,
points = array_subindex(pathfixed, [0:dim-1]),
parm = array_subindex(pathfixed, dim),
// dk will be a list of parameters, for the "smooth" type the distance and curvature parameter pair,
// and for the circle type, distance and radius.
dk = [for(i=[0:len(points)-1]) let(
angle = pathangle(wrap_range(points,i-1,i+1))/2,
parm0 = is_list(parm[i]) ? parm[i][0] : parm[i],
k = curve=="circle" && type=="radius" ? parm0 :
curve=="circle" && type=="cut" ? parm0 / (1/sin(angle) - 1) :
is_list(parm[i]) && len(parm[i])==2 ? parm[i][1] : default_curvature
) !closed && (i==0 || i==len(points)-1) ? [0,0] :
curve=="circle" ? [k/tan(angle), k] :
curve=="smooth" && type=="joint" ? [parm0,k] :
[8*parm0/cos(angle)/(1+4*k),k]
],
lengths = [for(i=[0:len(points)]) norm(flatten(diff(wrap_range(points, i-1,i))))],
scalefactors = [for(i=[0:len(points)-1])
min(lengths[i]/sum(array_subindex(wrap_range(dk,i-1,i),0)),
lengths[i+1]/sum(array_subindex(wrap_range(dk,i,i+1),0)))]
)
echo("Roundover scale factors:",scalefactors)
assert(min(scalefactors)>=1,"Curves are too big for the path")
[ for(i=[0:len(points)-1])
each dk[i][0] == 0 ? [points[i]] :
curve=="smooth" ? bezcorner(wrap_range(points,i-1,i+1), dk[i]) :
circlecorner(wrap_range(points,i-1,i+1), dk[i])
];
function bezcorner(points, parm) =
let(
d = parm[0],
k = parm[1],
prev = normalize(points[0]-points[1]),
next = normalize(points[2]-points[1]),
P = [points[1]+d*prev,
points[1]+k*d*prev,
points[1],
points[1]+k*d*next,
points[1]+d*next])
bezier_curve(P,200);
function circlecorner(points, parm) =
let(
angle = pathangle(points)/2,
d = parm[0],
r = parm[1],
prev = normalize(points[0]-points[1]),
next = normalize(points[2]-points[1]),
center = r/sin(angle) * normalize(prev+next)+points[1]
)
circular_arc(center, points[1]+prev*d, points[1]+next*d, 200);
// Compute points for the shortest circular arc that is centered at
// the specified center, starts at p1, and ends on the vector
// p2-center. The radius is the length of (p1-center). If (p2-center)
// has the same length then the arc will end at p2.
function circular_arc(center, p1, p2, N) = let(
angle = pathangle([p1,center,p2]),
v1 = p1-center,
v2 = p2-center
)
len(center)==2 ?
let(dir = sign(v1.x*v2.y-v1.y*v2.x), // z component of cross product
r=norm(v1))
assert(dir != 0, "Colinear inputs don't define a unique arc")
[for(i=[0:N-1])
let(theta=atan2(v1.y,v1.x)+i*dir*angle/(N-1))
r*[cos(theta),sin(theta)]+center] :
let(axis = cross(v1,v2))
assert( axis != [0,0,0], "Colinear inputs don't define a unique arc")
[for(i=[0:N-1])
matrix3_rot_by_axis(axis, i*angle/(N-1)) * v1 + center];
function bezier_curve(P,N) =
[for(i=[0:N-1]) bez_point(P, i/(N-1))];
// Compute the angle at a corner in a path using the law of cosines
function pathangle(pts) =
let( d = [for(i=[0:2]) norm(flatten(diff(wrap_range(pts,i,i+1))))] )
acos(constrain(
(d[0]*d[0] + d[1]*d[1] - d[2]*d[2]) / 2 / d[0] / d[1], -1,1));
function array_subindex(vect, index) =
[for(entry=vect)
let(value=[for(i=index) entry[i]])
len(value)==1 ? value[0] : value];
function array_zip(vecs,v2,v3) =
v3!=undef ? array_zip([vecs,v2,v3]) :
v2!=undef ? array_zip([vecs,v2]) :
let(
length = len(vecs[0]),
samesize = [for (v=vecs) len(v)==length?0:1],
dummy=assert(sum(samesize)==0,"Input vectors must have the same length")
)
[for(i=[0:length-1])
[for(v=vecs) each v[i]]
];
function replist(list, N) = [for(i=[0:N-1]) list];
function diff(v) =
[for(i=[0:len(v)-2]) v[i+1]-v[i]];
function wrap(list,index,index2) =
let (L = len(list),
indices = index2 == undef ? index :
let (a=index % L + L,
b=index2 % L + L)
b<a ? [a:b+L] : [a:b])
is_num(indices) ? list[((indices % L)+L) % L] :
[for(i=indices) list[((i % L)+L) % L]];
// Return the array dimensions excluding the first one (which is just the length of the list)
//
function _array_dim_recurse(v) =
// If first entry is not a list then v is either a list of singletons, in which case we return []
// because there is no 2nd dimension, or if some entry is a list we return undef
!is_list(v[0]) ?
(sum( [for(entry=v) is_list(entry) ? 1 : 0]) == 0 ? [] : [undef] ) :
// First entry is a list so check for length consistency of the list entries, and then
// recursively call to get the rest of the dimensions
let( firstlen = len(v[0]),
first = sum( [for(entry = v) len(entry) == firstlen ? 0 : 1] ) == 0 ? firstlen : undef,
leveldown = flatten(v) )
is_list(leveldown[0]) ? concat([first],_array_dim_recurse(leveldown)) : [first];
//
function array_dim(v,depth) =
// Add the first dimension entry (the length) to the rest of the values
depth == undef ? concat([len(v)], _array_dim_recurse(v)) :
// A depth was requested, so compute the dimension list and then return
// the requested entry.
depth == 0 ? len(v) :
let(
dimlist = _array_dim_recurse(v)
)
depth > len(dimlist) ? 0 : dimlist[depth-1];