Git Product home page Git Product logo

belfryscad / bosl2 Goto Github PK

View Code? Open in Web Editor NEW
838.0 22.0 106.0 8.38 MB

The Belfry OpenScad Library, v2.0. An OpenSCAD library of shapes, masks, and manipulators to make working with OpenSCAD easier. BETA

Home Page: https://github.com/BelfrySCAD/BOSL2/wiki

License: BSD 2-Clause "Simplified" License

OpenSCAD 99.47% Python 0.29% Shell 0.13% CSS 0.07% Lua 0.04%
scad openscad-library openscad openscad-framework

bosl2's Introduction

BOSL2

BOSL2 Logo

The Belfry OpenScad Library, v2

A library for OpenSCAD, filled with useful tools, shapes, masks, math and manipulators, designed to make OpenSCAD easier to use.

Requires OpenSCAD 2021.01 or later.

  • NOTE: BOSL2 IS BETA CODE. THE CODE IS STILL BEING REORGANIZED.
  • NOTE2: CODE WRITTEN FOR BOSLv1 PROBABLY WON'T WORK WITH BOSL2!

Join the chat at https://gitter.im/revarbat/BOSL2

Documentation

You can find the full BOSL2 library documentation at: https://github.com/BelfrySCAD/BOSL2/wiki

Installation

  1. Download the .zip or .tar.gz release file for this library. Currently you should be able to find this at https://github.com/BelfrySCAD/BOSL2/archive/refs/heads/master.zip
  2. Unpack it. Make sure that you unpack the whole file structure. Some zipfile unpackers call this option "Use folder names". It should create either a BOSL-v2.0 or BOSL2-master directory with the library files within it. You should see "examples", "scripts", "tests", and other subdirectories.
  3. Rename the unpacked main directory to BOSL2.
  4. Move the BOSL2 directory into the apropriate OpenSCAD library directory. The library directory may be on the list below, but for SNAP or other prepackaged installations, it is probably somewhere else. To find it, run OpenSCAD and select Help→Library Info, and look for the entry that says "User Library Path". This is your default library directory. You may choose to change it to something more convenient by setting the environment variable OPENSCADPATH. Using this variable also means that all versions of OpenSCAD you install will look for libraries in the same location.
    • Windows: My Documents\OpenSCAD\libraries\
    • Linux: $HOME/.local/share/OpenSCAD/libraries/
    • Mac OS X: $HOME/Documents/OpenSCAD/libraries/
  5. Restart OpenSCAD.

Examples

A lot of the features of this library are to allow shorter, easier-to-read, intent-based coding. For example:

BOSL2/transforms.scad Examples Raw OpenSCAD Equivalent
up(5) translate([0,0,5])
xrot(30,cp=[0,10,20]) translate([0,10,20]) rotate([30,0,0]) translate([0,-10,-20])
xcopies(20,n=3) for (dx=[-20,0,20]) translate([dx,0,0])
zrot_copies(n=6,r=20) for (zr=[0:5]) rotate([0,0,zr*60]) translate([20,0,0])
skew(sxz=0.5,syz=0.333) multmatrix([[1,0,0.5,0],[0,1,0.333,0],[0,0,1,0],[0,0,0,1]])
BOSL2/shapes.scad Examples Raw OpenSCAD Equivalent
cube([10,20,30], anchor=BOTTOM); translate([0,0,15]) cube([10,20,30], center=true);
cuboid([20,20,30], rounding=5); minkowski() {cube([10,10,20], center=true); sphere(r=5, $fn=32);}
prismoid([30,40],[20,30],h=10); hull() {translate([0,0,0.005]) cube([30,40,0.01], center=true); translate([0,0,9.995]) cube([20,30,0.01],center=true);}
xcyl(l=20,d=4); rotate([0,90,0]) cylinder(h=20, d=4, center=true);
cyl(l=100, d=40, rounding=5); minkowski() {cylinder(h=90, d=30, center=true); sphere(r=5);}
tube(od=40,wall=5,h=30); difference() {cylinder(d=40,h=30,center=true); cylinder(d=30,h=31,center=true);}
torus(d_maj=100, d_min=30); rotate_extrude() translate([50,0,0]) circle(d=30);

bosl2's People

Contributors

adrianvmariano avatar blayzeing avatar bradenm avatar cdc-mkb avatar dimo414 avatar geoffder avatar gitter-badger avatar greenellipsis avatar hegjon avatar hvegh avatar kelvie avatar kevinboulain avatar laenzlinger avatar lar3ry avatar lettore avatar martincizek avatar matthewfallshaw avatar mvirkkunen avatar ochafik avatar pipatron avatar pwclay avatar ramilewski avatar revarbat avatar ronaldocmp avatar stockholmux avatar thehans avatar tomas-pecserke avatar typetetris avatar westminsterflip avatar yawor avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

bosl2's Issues

arc

function arc(N,center,r,d,start,angle,points) =
    // First try for 2d arc specified by angles
    is_def(center) && is_def(start) ?
       let(parm_ok = is_undef(points) && 
                     (is_vector(start) && len(start)==2 && is_undef(angle) ||
                      is_num(start) && is_num(angle)))
       assert(parm_ok,"Invalid parameters")
       let(
       angle = is_def(angle) ? angle : start[1]-start[0],
       start = is_vector(start) ? start[0] : start,
       radius = get_radius(r=r,d=d),
       N = max(3,N))
       [for(i=[0:N-1]) let(theta = start + i*angle/(N-1)) r*[cos(theta),sin(theta)]+center] :
    assert(is_list(points),"Invalid parameters")
    // Arc is 3d, so transform points to 2d and make a recursive call, then remap back to 3d
    len(points[0])==3 ?
      let(
        thirdpoint = is_def(center) ? center : points[2],
        center2d = is_def(center) ? xyz_to_planar(center,thirdpoint,points[0],points[1]) : undef,
        points2d = new_xyz_to_planar(points,thirdpoint,points[0],points[1])
      )
      new_planar_to_xyz(arc(N,center=center2d,points=points2d),thirdpoint,points[0],points[1]) :
    // Arc defined by center plus two points, will have radius defined by center and points[0]
    // and extent defined by direction of point[1] from the center
    is_def(center) ?
      let(
         angle = pathangle([points[0],center,points[1]]),
         v1 = points[0]-center,
         v2 = points[1]-center,
         dir = sign(det2([v1,v2])),   // z component of cross product
         r=norm(v1)
       )
       assert(dir!=0,"Collinear inputs don't define a unique arc")
       arc(N,center,r,start=atan2(v1.y,v1.x),angle=dir*angle) :
     // Final case is arc passing through three points, starting at point[0] and ending at point[3]
     let(col = collinear(points[0],points[1],points[2],1e-3))
     assert(!col, "Collinear inputs do not define an arc")
     let(
        center = line_intersection(normal_segment(points[0],points[1]),normal_segment(points[1],points[2])),
             // select order to be counterclockwise
        dir=det2([points[1]-points[0],points[2]-points[1]])>0,
        points = dir ? select(points,[0,2]) : select(points,[2,0]),  
        r = norm(points[0]-center),
        theta_start = atan2(points[0].y-center.y, points[0].x-center.x),
        theta_end =  atan2(points[1].y-center.y, points[1].x-center.x),
        angle = posmod(theta_end-theta_start,360),
        arcpts = arc(N,center,r,start=theta_start,angle=angle)
     )
     dir ? arcpts:reverse(arcpts);

Dependencies are in the pathoffset and rounded extrude code. It seems like I work too hard to figure out the angles, but I also found it remarkably hard to get that robust, so that my arc goes the right way. I tested on a bunch of random triples and didn't notice any problems.

hull functions

I took a look at the hull functions and things seem to be working as expected generally. I have the following comments:

  • In openscad base language, we have hull(), not hull2d and hull3d. What is gained by splitting this into two functions instead of supplying simply hull_points() that does both cases in perfect parallel with the base language?
  • I think it would be nicer to wrap the "fast" version in as an option, so hull_points(points, fast=true) would activate it instead of having a separate function for it.
  • The colinearity check increases run time for me from 6s to 36s on 5e5 points input. One could argue that nobody will ever need to run on such a large set of points, I guess, but it is an unnecessary check.
  • If the number of points isn't divisible by 3 the last one is skipped in hull3d_points_fast. You need to change -3 to -2 in the loop. I wonder if running the loop to step off by 2's would decrease the run-time penalty for checking colinearity. (The end effect would be complicated.)
  • Have you observed a test case where it the "fast" code prints a warning?
  • Document that the hull functions can handle about 4k points whereas hull_fast can handle unlimited points (up to max array size, I think)
  • The documentation doesn't mention the outcome in the colinear case, which for hull*_points is an error()
  • Actually it looks like the colinear case is broken. In 2d the original linde call works and produces the endpoints and the BOSL function produces incorrect output. In 3d it appears that the original version and the linde version both produce the same incorrect output (a set of four 3-vertex faces).

sorting (still) kinda slow (especially sortidx)

I'm not sure what can/should be done, but here are some time tests:

quicksort (from the manual) 9s, BOSL2: 33s, sortidx: 81s. That is with already sorted lists of 200k items. With shuffled lists times are 12s, 43s, and 110s respectively.

Does it make sense to special case the sort for plain vectors?

add roundcorners

I'm not sure that "curve" and "type" are the best designed interface.

use<BOSL2/std.scad>
use<BOSL2/beziers.scad>

// Function: roundcorners()
//
// Description:
//   Takes a 2d or 3d point list as input (a path or the points of a polygon) and rounds each corner by a specified amount.
//   The rounding at each point can be different and some points can have zero rounding.  The roundcorners() function supports
//   two types of rounding, circular rounding and continuous curvature rounding using 4th order bezier curves.
//   Circular rounding can produce a tactile "bump" where the curvature changes from flat to circular.
//   See https://hackernoon.com/apples-icons-have-that-shape-for-a-very-good-reason-720d4e7c8a14
//
//   You select the type of rounding using the `curve` option, which should be either "smooth" to get continuous curvature 
//   rounding or "circle" to get circular rounding.  Each rounding method has two options for how you specify the amount of rounding, which
//   you select using the `type` argument.  Both rounding methods accept `type="cut"`.  This mode specifies the amount of rounding
//   as the distance from the corner to the curve.  This can be easier to understand than setting a circular radius, which can
//   be unexpectedly extreme when the corner is very sharp.  It also allows a systematic specification of curves that is the same
//   for both "circle" and "smooth".
//
//   The second `type` setting for circular rounding is "radius", which sets a circular rounding radius.  The second `type`
//   setting for smooth rounding is "joint" which specifies the distance away from the corner where the roundover should start.
//   The "smooth" type rounding also has a parameter that specifies how smooth the curvature match is.  This parameter ranges
//   from 0 to 1, with a default of 0.5.  Larger values give a more abrupt transition and smaller ones a more gradual transition.
//   If you set the value much higher than 0.8 the curvature changes abruptly enough that though it is theoretically continuous, it may not
//   be continous in practice.  If you set it very small then the transition is so gradual that the length of the roundover may be extremely long. 
//
//   If you select curves that are too large to fit the function will fail with an error.  It displays a set of scale factors that you can apply to 
//   the (first) smoothing parameter that will reduce the size of the curves so that they will fit on your path.  If the scale factors are larger than
//   one then they indicate how much you can increase the curve sizes before collisions will occur.  
//
//   To specify rounding parameters you can use the `all` option to round every point in a path.
//   Examples:
//   * `curve="circle", type="radius", all=2`: Rounds every point with circular, radius 2 roundover
//   * `curve="smooth", type="cut", all=2`: Rounds every point with continuous curvature rounding with a cut of 2, and a default 0.5 smoothing parameter
//   * `curve="smooth", type="cut", all=[2,.3]`: Rounds every point with continuous curvature rounding with a cut of 2, and a very gentle 0.3 smooth setting
//
//   The path is a list of 2d or 3d points, possibly with an extra coordinate giving smoothing parameters.  It is important to specify 
//   if the path is a closed path or not using the `closed` parameter.  The default is a closed path for making polygons.  
//   Path examples:
//   * [[0,0],[0,1],[1,1],[0,1]]: 2d point list (a square), `all` was given to set rounding
//   * [[0,0,0], [0,1,1], [1,1,2], [0,1,3]]: 3d point list, `all` was given to set rounding
//   * [[0,0,0.2],[0,1,0.1],[1,1,0],[0,1,0.3]]: 2d point list with smoothing parameters different at every corner, `all` not given
//   * [[0,0,0,.2], [0,1,1,.1], [1,1,2,0], [0,1,3,.3]]: 3d point list with smoothing parameters, `all` not given
//   * [[0,0,[.3,.7], [4,0,[.2,.6]], [4,4,0], [0,4,1]]: 3d point list with smoothing parameters for the "smooth" type roundover, `all` not given.  Note the third entry is sometimes a pair giving both smoothing parameters, sometimes it's zero specifying no smoothing, and sometimes a single number, specifying the amount of smoothing but using the default smoothness parameter.  
//
//   The number of segments used for roundovers is determined by $fa, $fs and $fn as usual for circular roundovers.  For continuous curvature roundovers $fs and $fn are used and $fa is ignored.  When doing continuous curvature rounding be sure to use lots of segments or the effect will be hidden by the discretization.  
//
// Arguments:
//   path = list of points defining the path to be rounded.  Can be 2d or 3d, and may have an extra coordinate giving rounding parameters.  If you specify rounding parameters you must do so on every point.  
//   curve = rounding method to use.  Set to "circle" for circular rounding and "smooth" for continuous curvature 4th order bezier rounding
//   type = rounding parameter type.  Set to "cut" to specify the cut back with either "smooth" or "circle" rounding methods.  Set to "radius" with `curve="circle"` to set circular radius rounding.  Set to "joint" with `curve="smooth"` for joint type rounding.  (See above for details on these rounding options.)
//   all = curvature parameter(s).  Set this to the curvature parameter or parameters to apply to all points on the list.  If you set this then all values given in the path are treated as geometrical coordinates.  If you don't set this then the last value of each entry in `path` is treated as a smoothing parameter.
//   closed = if true treat the path as a closed polygon, otherwise treat it as open.  Default: true.
//
// Example: Standard circular roundover with radius the same at every point. Compare results at the different corners.  
//   shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
//   polygon(roundcorners(shape, curve="circle", type="radius", all=1));
//   %down(.1)polygon(shape);
// Example: Circular roundover using the "cut" specification, the same at every corner.  
//   shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
//   polygon(roundcorners(shape, curve="circle", type="cut", all=1));
//   %down(.1)polygon(shape);
// Example: Circular roundover using the "cut" specification, the same at every corner.  
//   shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
//   polygon(roundcorners(shape, curve="circle", type="cut", all=1));
//   %down(.1)polygon(shape);
// Example: Continous curvature roundover using "cut", still the same at every corner.  The default smoothness parameter of 0.5 was too gradual for these roundovers to fit, but 0.7 works.  
//   shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
//   polygon(roundcorners(shape, curve="smooth", type="cut", all=[1,.7]));
//   %down(.1)polygon(shape);
// Example: Continuous curvature roundover using "joint", for the last time the same at every corner.  Notice how small the roundovers are.  
//   shape = [[0,0], [10,0], [15,12], [6,6], [6, 12], [-3,7]];
//   polygon(roundcorners(shape, curve="smooth", type="joint", all=[1,.7]));
//   %down(.1)polygon(shape);
// Example: Circular rounding, different at every corner, some corners left unrounded
//   shape = [[0,0,1.8], [10,0,0], [15,12,2], [6,6,.3], [6, 12,1.2], [-3,7,0]];
//   polygon(roundcorners(shape, curve="circle", type="radius"));
//   %down(.1)polygon(array_subindex(shape,[0:1]));
// Example: Continuous curvature rounding, different at every corner, with varying smoothness parameters as well, and $fs set very small
//   shape = [[0,0,[1.5,.6]], [10,0,0], [15,12,2], [6,6,[.3,.7]], [6, 12,[1.2,.3]], [-3,7,0]];
//   polygon(roundcorners(shape, curve="smooth", type="cut", $fs=0.1));
//   %down(.1)polygon(array_subindex(shape,[0:1]));
// Example: 3d printing test pieces to display different curvature shapes.  You can see the discontinuity in the curvature on the "C" piece in the rendered image.  
//   $fn=200;
//   ten = [[0,0,5],[50,0,5],[50,50,5],[0,50,5]];
//   linear_extrude(height=14){
//   translate([25,25,0])text("C",size=30, valign="center", halign="center");
//     translate([85,25,0])text("5",size=30, valign="center", halign="center");
//     translate([85,85,0])text("3",size=30, valign="center", halign="center");
//     translate([25,85,0])text("7",size=30, valign="center", halign="center");
//   }
//   linear_extrude(height=13)
//   {
//     polygon(roundcorners(ten, curve="circle", type="cut"));
//     translate([60,0,0])polygon(roundcorners(ten,  curve="smooth", type="cut"));
//     translate([60,60,0])polygon(roundcorners([[0,0],[50,0],[50,50],[0,50]],all=[5,.32],$fs=5,$fa=0,
//                                             curve="smooth", type="cut"));
//     translate([0,60,0])polygon(roundcorners([[0,0],[50,0],[50,50],[0,50]],all=[5,.7],
//                                             curve="smooth", type="cut"));
//   }   

function roundcorners(path, curve, type, all=undef,  closed=true) =
  let(
    default_curvature = 0.5,   // 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(select(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(select(points,i)-select(points,i-1))],
    scalefactors = [for(i=[0:len(points)-1])
      min(lengths[i]/sum(array_subindex(select(dk,i-1,i),0)),
          lengths[i+1]/sum(array_subindex(select(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(select(points,i-1,i+1), dk[i]) :
                                circlecorner(select(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],
     N = $fn>0 ? max(3,$fn) :
          ceil(bezier_segment_length(P)/$fs)
    )
    bezier_curve(P,N);


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, 300);


// 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,
   N = ceil(angle/360) * segs(norm(v1))
   )
   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))];

function pathangle(pts) = vector_angle(pts[0]-pts[1], pts[2]-pts[1]);

right_of_line2d vs point_left_of_segment

These two functions basically do the same thing, except that point_left_of_segment detects the equality case. It seems like we don't need both. point_left_of_segment could be renamed point_left_of_line, maybe? Or maybe something else that conveys the idea of the point's relationship to a line? (Nothing leaps to mind.) In any case, the docs also need to be beefed up to define what "left" means---in other words, that direction is defined by the order of the points that specify the line.

Note that right_of_line2d is never used.

multi-point plane projection

function project_plane(point, a, b, c) =
	let(
		u = normalize(b-a),
		v = normalize(c-a),
		n = normalize(cross(u,v)),
		w = normalize(cross(n,u)),
		relpoint = is_vector(point) ? point-a : translate_points(point,-a)
	) relpoint * transpose([w,u]);


function lift_plane(point, a, b, c) = let(
	u = normalize(b-a),
	v = normalize(c-a),
	n = normalize(cross(u,v)),
	w = normalize(cross(n,u)),
        remapped = point*[w,u]
) is_vector(remapped) ? a + remapped : translate_points(remapped,a);

drop assertion compatibility function? Fix assert_in_list to avoid dummy variable?

Since you mentioned dropping compatibility functions I assume there's no reason to keep assertion() around.

Also, assert_in_list bugs me because it forces the use of dummy variables, which are not necessary with the regular assert() call. It seems like this should be reworked so that the in_list error can be asserted without the dummy variable by passing something to assert() instead of having the function do the work. (Doing this will presumably also get rid of duplicated code in the module vs function.)

add support for structures

This is already needed by path_offset and is also used by the new screw code. Have I overlooked any needed functions? Note also that struct keywords don't have to be strings, but can be numbers, or even lists. I was just noticing this. Not sure if lists is useful or not. But I can imagine it might be. You could lookup point values in 3d, for example...if everything is exact.

// Function: struct_set()
// Usage:
//    struct_set(struct, keyword, value)
// Description:
//    Sets the keyword in the structure to the specified value, returning a new updated structure. If the keyword
//    exists its value is changed, otherwise the keyword is added.  The keyword can be any type.
// Arguments:
//    struct = input structure
//    keyword = keyword to set, can be any type
//    value = value to set the keyword to
function struct_set(struct, keyword, value) = 
     let(ind=search([keyword],struct,1,0)[0])
     ind==[] ? concat(struct, [[keyword,value]]) :
               list_set([ind], [[keyword,value]],struct);

// Function: struct_remove()
// Usage:
//    struct_remove(struct, keyword)
// Description:
//    Remove keyword or keyword list from a structure
// Arguments:
//    struct = input structure
//    keyword = a single string (keyword) or list of strings (keywords) to remove
function struct_remove(struct, keyword) =
    is_string(keyword)? struct_remove(struct, [keyword]) :
    let(ind = search(keyword, struct))
     list_remove(struct, ind);

// Function: struct_val()
// Usage:
//    struct_val(struct,keyword)
// Description:
//    Returns the value for the specified keyword in the structure, or undef if the keyword is not present
// Arguments:
//    struct = input structure
//    keyword = keyword whose value to return
function struct_val(struct,keyword) = 
     let(ind = search([keyword],struct)[0])
     ind == [] ? undef : struct[ind][1];

// Function: struct_keys()
// Usage:
//    struct_keys(struct)
// Description:
//    Returns a list of the keys in a structure
// Arguments:
//    struct = input structure
function struct_keys(struct) =
     [for(entry=struct) entry[0]];

// Function: parse_pairs()
// Usage:
//    parse_pairs(spec, input)
// Description:
//    Parse a list of the form [keyword1, value1, keyword2, value2, ...] where the spec
//    provides a list of accepted keywords and default values.  The output is a structure with the specified keywords and input values or defaults.  
// Arguments:
//    spec = specification for keywords and defaults, a list of the form [[keyword1,default1],[keyword2,default2], ...]  A keyword that is not in the specification produces an error.
//    input = flat list of pairs of the form [keyword1, value1, keyword2, value2, ...]
function parse_pairs(spec, input, index=0, result=undef) = 
     let( result = result==undef ? spec : result)
     index == len(input) ? result :
     let(item = search([input[index]], spec,1,0)[0])
     assert(item!=[], str("Unknown keyword ",input[index]))
     assert(len(input)>index+1,str("Keyword ",input[index]," has no value"))
   parse_pairs(spec,input,index+2, struct_set(result, input[index], input[index+1]));

// Function&Module struct_echo()
// Usage:
//    struct_echo(struct, [name])
// Description:
//    Displays a list of structure keywords and values, one pair per line, for easier reading.
// Arguments:
//    struct = input structure
//    name = optional structure name to list at the top of the output.  Default: ""
function struct_echo(struct,name="") =
   let( keylist = [for(entry=struct) str("&nbsp;&nbsp;",entry[0],": ",entry[1],"\n")])
   echo(str("\nStructure ",name,"\n",strcat(keylist)))
   undef;

module struct_echo(struct,name="") {
   dummy = struct_echo(struct,name);
}

list_pad() is slow as written. Using concat is much faster

I did a test on a large list and my run time was 54 s, to lengthen a large list by a small amount.

Using concat is much faster at only 3 seconds:

function list_pad(v, minlen, fill=undef) =
	concat(v,replist(fill,minlen-len(v)));

In a test of extended a short list by a large amount the existing code took 33s and the concat version takes 19s. So the concat version is always better. This second result was a bit of a surprise, since replist just runs a for loop, but I guess the fact that there's no conditional test in the loop is making the difference?

I also note that list_trim can be written without the maxlen<1 test by adding a step of 1 into the range. And the documentation erroneously lists "minlen" as an argument. And with list_pad rewritten as above, I think list_fit doesn't need the test for l==length. That case is handled without extra run time cost by letting list_pad do it, where it pads on the empty list. Then you can avoid the extra assignment and again the code is shorter and simpler.

I also kind of feel like it makes more sense to supply list_lengths() and let users take max or min of the result instead of list_longest and list_shortest. Hmmm. Is there a way to get the index of the max or min? I guess you can run search after running min/max.

preliminary pathoffset and rounded_extrude

Check this out and let me know what you think. It seems like something wrong with bezier functions in bosl2. I had to change the include in roundcorners to the old version or I got errors, but no time to troubleshoot.

use<BOSL2/transforms.scad>
use<BOSL2/std.scad>

// Opposite of bselect:  Sets array[i] if indexset[i] is true, taking values sequentially from valuelist 
function array_set(indexset, valuelist, _i=0, _v=0,array=[]) = 
            _i==len(indexset) ? array : 
            array_set(indexset, valuelist, _i+1, _v+(indexset[_i]?1:0), concat(array, [ indexset[_i]?valuelist[_v]:0]));

// index is a boolean array.  Return a list where array[i] is on the list if index[i] is true
function bselect(array,index) = [for(i=[0:len(array)-1]) if (index[i]) array[i]];

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 is_def(x) = !is_undef(x);

// Compute offset to the input path.
// The offset is given as delta or r (as is done with the offset command)
// and a positive value shifts to the left of the path direction and a
// negative value to the right.
//
// Specify closed=true if you want a closed curve
//
// maxstep specifies the maximum step size for roundovers.  



// Function: pathoffset()
//
// Description:
//   Takes an input path and returns a path offset by the specified amount.  As with offset(), you can use
//   r to specify rounded offset and delta to specify offset with corners.  Positive offsets shift the path
//   to the left (relative to the direction of the path).
//
//   When offsets shrink the path, segments cross and become invalid.  By default pathoffset checks for this situation. 
//   This check takes O(N^2) time and can be disabled if you don't need it by setting check_valid=false.
//   In some cases the check may mistakenly eliminate segments that should have been included.  If this
//   happens, increase `quality` to 2 or 3.  Run time will increase.  You may be able to save run time
//   in some cases by setting `quality` to 0.
//
//   For construction of polyhedra pathoffset() can also return face lists.  These list faces between the 
//   original path and the offset path where the vertices are ordered with the original path first,
//   starting at `firstface_index` and the offset path vertices appearing afterwords.  The direction
//   of the faces can be flipped using `flip_faces`.  When you request faces the return value
//   is a list: [offset_path, face_list].  
//
// Arguments:
//   path = the path to process.  A list of 2d points.
//   r = offset radius.  Distance to offset.  Will round over corners.
//   delta = offset distance.  Distance to offset with pointed corners.
//   closed = path is a closed curve. Default: False.
//   check_valid = perform segment validity check.  Default: True.
//   quality = validity check quality parameter, a small integer.  Default: 1.
//   return_faces = return face list.  Default: False.
//   firstface_index = starting index for face list.  Default: 0.
//   flip_faces = flip face direction.  Default: false
function pathoffset(path, r=undef, delta=undef, maxstep=.1, closed=false, check_valid=true, quality=1,
                    return_faces=false, firstface_index=0, flip_faces=false) =
   let(rcount = len(remove_undefs([r,delta])))
   assert(rcount==1,"Must define exactly one of 'delta' and 'r'")
   let(
     quality = max(0,round(quality)),
     d = is_def(r)? r : delta,
     shiftsegs = [for(i=[0:len(path)-1]) _shift_segment(select(path,i,i+1), d)],
     // good segments are ones where no point on the segment is less than distance d from any point on the path
     good = check_valid ? _good_segments(path, abs(d), shiftsegs, closed, quality) : replist(1,len(shiftsegs)),
     goodsegs = bselect(shiftsegs, good),
     goodpath = bselect(path,good)
   )
   assert(len(goodsegs)>0,"Offset of path is degenerate")
   let(
        // Extend the shifted segments to their intersection points
     sharpcorners = [for(i=[0:len(goodsegs)-1]) _segment_extension(select(goodsegs,i-1), select(goodsegs,i))],
        // If some segments are parallel then the extended segments are undefined.  This case is not handled
     parallelsegs = len(sharpcorners)!=len(remove_undefs(sharpcorners))
   )
   assert(!parallelsegs, "Path turns back on itself (180 deg turn)")
   let(
        // This is a boolean array that indicates whether a corner is an outside or inside corner
        // For outside corners, the newcorner is an extension (angle 0), for inside corners, it turns backward
        // If either side turns back it is an inside corner---must check both.  
        // Outside corners can get rounded (if r is specified and there is space to round them)
     outsidecorner = [for(i=[0:len(goodsegs)-1]) let(prevseg=select(goodsegs,i-1))
                                                (goodsegs[i][1]-goodsegs[i][0]) * (goodsegs[i][0]-sharpcorners[i]) > 0 
                                                && (prevseg[1]-prevseg[0]) * (sharpcorners[i]-prevseg[1]) > 0 ],
     steps = is_def(delta) ? [] : [for(i=[0:len(goodsegs)-1])
             ceil(abs(r)*vector_angle(select(goodsegs,i-1)[1]-goodpath[i], goodsegs[i][0]-goodpath[i])*PI/180/maxstep)],
        // If rounding is true then newcorners replaces sharpcorners with rounded arcs where needed
        // Otherwise it's the same as sharpcorners
        // If rounding is on then newcorners[i] will be the point list that replaces goodpath[i] and newcorners later
        // gets flattened.  If rounding is off then we set it to [sharpcorners] so we can later flatten it and get
        // plain sharpcorners back.
     newcorners = is_def(delta) ? [sharpcorners] : [for(i=[0:len(goodsegs)-1]) 
            steps[i] <=2 ||       // Don't round if steps <=2
            !outsidecorner[i] ||  // Don't round inside corners
              (!closed && (i==0 || i==len(goodsegs)-1)) ? [sharpcorners[i]]:
            circular_arc(goodpath[i], select(goodsegs,i-1)[1], goodsegs[i][0], steps[i])],
     pointcount = is_def(delta) ? replist(1,len(sharpcorners)) : [for(i=[0:len(goodsegs)-1]) len(newcorners[i])],
     start = [goodsegs[0][0]],
     end = [goodsegs[len(goodsegs)-2][1]],
     edges =  closed ? flatten(newcorners) : concat(start,slice(flatten(newcorners),1,-2),end),
     faces = return_faces ? _makefaces(flip_faces, firstface_index, good, pointcount, closed) : []
    )
    return_faces ? [edges,faces] : edges;

function _shift_segment(segment, d) = 
   translate_points(segment,d*normalize([segment[0].y-segment[1].y, segment[1].x-segment[0].x]));

// Extend to segments to their intersection point.  First check if the segments already have a point in common,
// which can happen if two colinear segments are input to pathoffset
function _segment_extension(s1,s2) = norm(s1[1]-s2[0])<1e-6 ? s1[1] :
                                     line_intersection(s1,s2);

function _makefaces(direction, startind, good, pointcount, closed) = 
   let( lenlist = array_set(good, pointcount),
        numfirst = len(lenlist),
        numsecond = sum(lenlist),
        prelim_faces = _makefaces_recurse(startind, startind+len(lenlist), numfirst, numsecond, lenlist, closed)
   )
   direction ? [for(entry=prelim_faces) reverse(entry)] : prelim_faces;


function _makefaces_recurse(startind1, startind2, numfirst, numsecond, lenlist, closed, firstind=0, secondind=0, faces=[]) =
     // We are done if *both* firstind and secondind reach their max value, which is the last point if !closed or one past
     // the last point if closed (wrapping around).  If you don't check both you can leave a triangular gap in the output.  
     firstind == numfirst - (closed?0:1) && secondind == numsecond - (closed?0:1) ? faces :
     _makefaces_recurse(startind1, startind2, numfirst, numsecond, lenlist, closed, firstind+1, secondind+lenlist[firstind],
       lenlist[firstind]==0 ?  // point in original path has been deleted in offset path, so it has no match.  We therefore
                               // make a triangular face using the current point from the offset (second) path
                               // (The current point in the second path can be equal to numsecond if firstind is the last point)
             concat(faces,[[secondind%numsecond+startind2, firstind+startind1, (firstind+1)%numfirst+startind1]]) :
         // in this case a point or points exist in the offset path corresponding to the original path     
             concat(faces, 
                     // First generate triangular faces for all of the extra points (if there are any---loop may be empty)
                [for(i=[0:1:lenlist[firstind]-2]) [firstind+startind1, secondind+i+1+startind2, secondind+i+startind2]],
                     // Finish (unconditionally) with a quadrilateral face
                [[firstind+startind1, (firstind+1)%numfirst+startind1,
                 (secondind+lenlist[firstind])%numsecond+startind2, (secondind+lenlist[firstind]-1)%numsecond+startind2]]
             )
     );

// Determine which of the shifted segments are good
function _good_segments(path, d, shiftsegs, closed, quality) =
    let(
        maxind = len(path)-(closed ? 1 : 2),
        pathseg = [for(i=[0:maxind]) select(path,i+1)-path[i]],
        pathseg_len =  [for(seg=pathseg) norm(seg)],
        pathseg_unit = [for(i=[0:maxind]) pathseg[i]/pathseg_len[i]],
          // Order matters because as soon as a valid point is found, the test stops
          // This order works better for circular paths because they succeed in the center
        alpha = concat([for(i=[1:1:quality]) i/(quality+1)],[0,1])
     )
    [for (i=[0:len(shiftsegs)-1]) _segment_good(path,pathseg_unit,pathseg_len, d - 1e-6, shiftsegs[i], alpha)];

// Determine if a segment is good (approximately)
// Input is the path, the path segments normalized to unit length, the length of each path segment
// the distance threshold, the segment to test, and the locations on the segment to test (normalized to [0,1])
// The last parameter, index, gives the current alpha index.  
//
// A segment is good if any part of it is farther than distance d from the path.  The test is expensive, so
// we want to quit as soon as we find a point with distance > d, hence the recursive code structure.
//
// This test is approximate because it only samples the points listed in alpha.  Listing more points
// will make the test more accurate, but slower.  
function _segment_good(path,pathseg_unit,pathseg_len, d, seg,alpha ,index=0) =
     index == len(alpha) ? false :
     _point_dist(path,pathseg_unit,pathseg_len, alpha[index]*seg[0]+(1-alpha[index])*seg[1]) > d ? true :
     _segment_good(path,pathseg_unit,pathseg_len,d,seg,alpha,index+1);


// Input is the path, the path segments normalized to unit length, the length of each path segment
// and a test point.  Computes the (minimum) distance from the path to the point, taking into
// account that the minimal distance may be anywhere along a path segment, not just at the ends. 
function _point_dist(path,pathseg_unit,pathseg_len,pt) =
   min(
     [for(i=[0:len(pathseg_unit)-1])
         let(
           v = pt-path[i],
           projection = v*pathseg_unit[i],
           segdist =  projection < 0 ? norm(pt-path[i]) :
                      projection > pathseg_len[i] ? norm(pt-select(path,i+1)) : 
                         norm(v-projection*pathseg_unit[i])
         ) segdist]);
   

function pathangle(pts) = vector_angle(pts[0]-pts[1], pts[2]-pts[1]);

function det2(M) = M[0][0] * M[1][1] - M[0][1]*M[1][0];


// Line intersection from two segments

// This function returns [p,t,u] where p is the intersection point of
// the lines defined by the two segments, t is the bezier parameter
// for the intersection point on s1 and u is the bezier parameter for
// the intersection point on s2.  The bezier parameter runs over [0,1]
// for each segment, so if it is in this range, then the intersection
// lies on the segment.  Otherwise it lies somewhere on the extension
// of the segment.

function _general_line_intersection(s1,s2) =
  let(  denominator = det2([s1[0],s2[0]]-[s1[1],s2[1]]),
        t=det2([s1[0],s2[0]]-s2)/denominator,
        u=det2([s1[0],s1[0]]-[s1[1],s2[1]])/denominator)
        [denominator==0 ? undef : s1[0]+t*(s1[1]-s1[0]),t,u];

// Returns the intersection point of the lines defined by two segments, or undef
// if the lines are parallel.  
function line_intersection(s1,s2) = let( int = _general_line_intersection(s1,s2)) int[0];

// Returns the intersection point of two segments and undef if they do not intersect
function segment_intersection(s1,s2) = let( int = _general_line_intersection(s1,s2))
        int[1]<0 || int[1]>1 || int[2]<0 || int[2]>1 ? undef : int[0];

// Returns the intersection point of a line defined by the first segment with a segment.
// If the segment doesn't intersect the line it returns undef
function line_segment_intersection(line,segment) = let( int = _general_line_intersection(line,segment))
         int[2]<0 || int[2]>1 ? undef : int[0];
        

// Return true if simple polygon is in clockwise order, false otherwise.
// Results for complex (self-intersecting) polygon are indeterminate
function polygon_clockwise(path) =
  let( 
       minx = min(array_subindex(path,0)),
       lowind = search(minx, path, 0, 0),
       lowpts = select(path, lowind),
       miny = min(array_subindex(lowpts, 1)),
       extreme_sub = search(miny, lowpts, 1, 1)[0],
       extreme = select(lowind,extreme_sub)
     )
  det2(  [select(path,extreme+1)-path[extreme], select(path, extreme-1)-path[extreme]])<0;


// End options
// simple rounded end
// roundover with specified radius and cutting back/extrapolation, including negative radius for flare
// "flat" with optional angle relative to object normal or relative to coordinate system
// "pointed" : [d,L], d is distance and L is extension
//
// what if path crosses itself (e.g. pentagram)?

function stroke(path, thickness, rounded=true, check_valid=false) = 
    let(
          left_r = !rounded ? undef : get_stroke_width(thickness, "left"), 
          left_delta = rounded ? undef : get_stroke_width(thickness, "left"), 
          right_r = !rounded ? undef : get_stroke_width(thickness, "right"), 
          right_delta = rounded ? undef : get_stroke_width(thickness, "right"), 
          left_path = pathoffset(path, delta=left_delta, r=left_r, closed=false, check_valid=check_valid),
          right_path = pathoffset(path, delta=right_delta, r=right_r, closed=false, check_valid=check_valid)
    )
    concat(left_path,reverse(right_path));

function get_stroke_width(thickness, side) =
  side=="left" ? (is_list(thickness) ? max(thickness) : thickness/2)
               : (is_list(thickness) ? min(thickness) : -thickness/2);

module stroke(path,thickness,rounded=true,closed=false,check_valid=true) {
    if (closed) {
    	  echo("vals",left_r, left_delta, right_r, right_delta);
          left_r = !rounded ? undef : get_stroke_width(thickness, "left"); 
          left_delta = rounded ? undef : get_stroke_width(thickness, "left");
          right_r = !rounded ? undef : get_stroke_width(thickness, "right"); 
          right_delta = rounded ? undef : get_stroke_width(thickness, "right");
          polygon1 = pathoffset(path, delta=left_delta, r=left_r, closed=true, check_valid=check_valid);
	  polygon2 = pathoffset(path, delta=right_delta, r=right_r, closed=true, check_valid=check_valid);
	  if (polygon_clockwise(path))
	    difference(){
	      polygon(polygon1);
	      polygon(polygon2);
	    }
	  else
	    difference(){
	      polygon(polygon2);
	      polygon(polygon1);
            }
    }
    else {
        echo("open case");
        polygon(stroke(path,thickness,rounded));
    }
        
}


module stroke2(path, width) {
    for (i=[1:len(path)-2])
        translate(path[i])
            circle(d=width);
    for (i=[0:len(path)-2])
        translate(path[i])
            rot(from=BACK, to=path[i+1]-path[i])
                left(width/2)
                    square([width, norm(path[i+1]-path[i])], center=false);
}

// Height is the total height
// It should be larger than r1+r2
//
// Negative value of r1 or r2 indicates flaring instead of rounding
//

function rounded_extrude_polyhedron(path,offsets, type, flip_faces, check_valid, offsetind=0, vertexcount=0, vertices=[], faces=[] )=
     offsetind==len(offsets) ?
         let( bottom = list_range(n=len(path),s=vertexcount),
              oriented_bottom = !flip_faces ? bottom : reverse(bottom)
           )
         [vertices, concat(faces,[oriented_bottom])] :
      let( this_offset = offsetind==0 ? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0],
           delta = type=="delta" ? this_offset : undef,
           r = type=="round" ? this_offset : undef,
           vertices_faces = pathoffset(path, r=r, delta=delta, closed=true, check_valid=check_valid,
                                      return_faces=true, firstface_index=vertexcount, flip_faces=flip_faces)
          )
        rounded_extrude_polyhedron(vertices_faces[0], offsets, type, flip_faces, check_valid, offsetind+1, vertexcount+len(path),
                                   vertices=concat(vertices, array_zip(vertices_faces[0],replist(offsets[offsetind][1],len(vertices_faces[0])))),
                                          faces=concat(faces, vertices_faces[1]));
  

function compute_offsets(radius, edgetype, N,sign=1,zoff=0,extra=0) =
   let( offsets =  edgetype == "chamfer" ? [[-radius,sign*abs(radius)+zoff]] :
                   edgetype == "teardrop" ? concat([for(i=[1:N]) [radius*(cos(i*45/N)-1),zoff+sign*abs(radius)* sin(i*45/N)]],
                                   [[-2*radius*(1-sqrt(2)/2), zoff+sign*abs(radius)]]):
                   /* round */  [for(i=[1:N]) [radius*(cos(i*90/N)-1), zoff+sign*abs(radius)*sin(i*90/N)]])
      extra > 0 ? concat(offsets, [select(offsets,-1)+[0,sign*extra]]) : offsets;


// Module: rounded_extrude()
//
// Description:
//   Takes a 2d path as input and extrudes it to a specified height with roundovers or chamfers at the ends.
//   The rounding is accomplished by using pathoffset to shift the input path.  The path is shifted multiple times
//   in sequence to produce the profile (not multiple shifts from one parent), so coarse definition of the input path will
//   multiply from the success shifts.  If the result seems rough try increasing the number of points you use for your input.
//   However, be aware that large numbers of points (especially when check_valid is true) can lead to lengthy run times.  
//   Multiple rounding shapes are available, including circular rounding, teardrop rounding, and chamfer "rounding".
//   Also note that if the rounding radius is negative then the rounding will project outwards.
//
//   Rounding options:
//     * "circle": Circular rounding with radius as specified
//     * "teardrop": Rounding using a 1/8 circle that then changes to a 45 degree chamfer.  The chamfer is at the end, and enables the object to be 3d printed without support.  The radius gives the radius of the circular part.
//     * "chamfer": Chamfer the edge at 45 degrees.  The radius specifies the height of the chamfer.  
//
// Arguments:
//   path = 2d path (list of points) to extrude
//   height = total height (including rounded portions) of the output
//   r1 = radius of roundover at bottom end (may be zero)
//   r2 = radius of roundover at top end (may be zero)
//   edge1 = type of rounding to apply to the bottom edge, one of "circle", "teardrop" or "chamfer".  Default: "circle"
//   edge2 = type of rounding to apply to top edge (same options as edge1).  Default: "circle"
//   offset_type1 = type of offset to use at bottom end, either "round" or "sharp".  Default: "round"
//   offset_type2 = type of offset to use at top end.  Default: "round"
//   check_valid = passed to pathoffset.  Default: True
//   steps = number of steps to use for roundovers.  
module rounded_extrude(path, height, r1, r2, edge1="circle", edge2="circle", extra1=0, extra2=0,
                       offset_type1="round", offset_type2="round", steps=20, check_valid=true)
{
  clockwise = polygon_clockwise(path);
  flipR = clockwise ? 1 : -1;
  assert(height>=0, "Height must be nonnegative");
  middle = height-abs(r1)-abs(r2);
  assert(middle>=0,"Specified radii are too large for extrusion height");
  
  offsets1 = r1==0 ? [] : compute_offsets(flipR*r1,edge1,steps,-1,0,extra1);
  initial_vertices_bot = array_zip(path, replist(0,len(path)));
  vertices_faces_bot = rounded_extrude_polyhedron(path, offsets1, offset_type1, clockwise,check_valid,vertices=initial_vertices_bot);

  top_start = len(vertices_faces_bot[0]);
  offsets2 = r2==0 ? [] : compute_offsets(flipR*r2,edge2,steps,1,middle,extra2);
  initial_vertices_top = array_zip(path, replist(middle,len(path)));
  vertices_faces_top = rounded_extrude_polyhedron(path, offsets2, offset_type2, !clockwise,check_valid,vertexcount=top_start, 
                                                  vertices=initial_vertices_top);
  middle_faces = middle==0 ? [] :
    [for(i=[0:len(path)-1]) let(oneface=[i, (i+1)%len(path), top_start+(i+1)%len(path), top_start+i])
                              !clockwise ? reverse(oneface) : oneface];
  up(abs(r1))
    polyhedron(concat(vertices_faces_bot[0],vertices_faces_top[0]),
               faces=concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces));
}


function ngon(n, side=undef, r=undef, d=undef, or=undef, od=undef, ir=undef, id=undef, cp=[0,0], scale=[1,1], align="side") =
  let(argcount = len(remove_undefs([side,r,d,or,od,ir,id])))
  assert(argcount==1,"You must specify exactly one of 'ir', 'id', 'or', 'od', 'r', 'd', and 'side'")
  let(
    dtheta = 360/n,
    theta_ofs = align=="side"? dtheta/2 : 0,
    factor = is_def(side) || is_def(or) || is_def(od) ? 1 : 1/cos(180/n),
    r = factor * (is_def(side) ? side/2/sin(180/n) :
                  is_def(d) ? d/2 :
                  is_def(id) ? id/2 :
                  is_def(ir) ? ir :
                  is_def(od) ? od /2 :
                  is_def(or) ? or :
                  r),
    ff=echo(factor=factor,r=r, prer = r/factor)
    )
  [for(theta=[theta_ofs:dtheta:360-1e-12]) r*[scale.x*cos(theta),scale.y*sin(theta)]+cp];


// Function: star()
//
// Description:      
//   Produce the coordinates of an n-pointed star with specified radius or diameter.  If you specify step
//   it creates a star that could be drawn by connecting n points on a circle with lines where each line
//   moves around the circle the specified number of steps.  Setting steps=2 creates a star with very blunt points;
//   The steps parameter must be smaller than n/2 and its maximum value produces the pointiest star.
//   Other stars can be created by specifying the inner radius or diameter. 
function star(n, r, d, ir, id, step, align="point") =
   let(
     r = get_radius(r=r, d=d),
     count = len(remove_undefs([ir,id,step])),
     stepOK = is_undef(step) || (step>1 && step<n/2)
   )
   assert(count==1, "Must specify exactly one if ir, id, step")
   assert(stepOK, str("Parameter 'step' must be between 2 and ",floor(n/2)," for ",n," point star"))
   let(
     ir = get_radius(r=ir, d=id, dflt=is_def(step)?cos(180*step/n)/cos(180*(step-1)/n):0),
     offset = align=="point" ? 0 : 180/n
   )
   [for(i=[0:2*n-1]) let(theta=180*i/n+offset, radius=(i%2)?ir:r) radius*[cos(theta), sin(theta)]];



/////////////////////////////////////////////////////////////////////////////
//
// End library functions
//
// Remaining stuff is testing calls

use<roundcorners.scad>
      
// Example 1 

box = [[0,0], [0,50], [255,50], [255,0]];
rbox = roundcorners(box, curve="smooth", type="cut", all=4, $fn=36);
difference(){
  rounded_extrude(rbox, height=50, r1=2, r2=1, steps = 22, edge1="teardrop", check_valid=false);
  up(2)rounded_extrude(pathoffset(rbox, r=-2, closed=true), height=48, r1=4, r2=-1,steps=22,extra2=1,check_valid=false);
}

smallbox = [[0,0], [0,50], [75,50], [75,0]];
roundbox = roundcorners(smallbox, curve="smooth", type="cut", all=4);
back(100)
difference(){
  rounded_extrude(roundbox, height=50, r1=2, r2=1, steps = 22, edge1="teardrop", check_valid=false);
  up(2)rounded_extrude(pathoffset(roundbox, r=-2, closed=true), height=48, r1=4, r2=-1,steps=22,extra2=1,check_valid=false);
}


// Example 2

// Path turns 180 deg; Case not allowed

/*
path = [[0,0],[5,0],[3,0],[3,3]];
echo(pathoffset(path, r=1, closed=false, check_valid=true));
*/

// Example 3

/*
test = [[0,0],[10,0],[10,7],[0,7], [-1,-3]];
polygon(pathoffset(test,r=1.9, closed=true, check_valid=true,quality=1));    // This succeeds
//polygon(pathoffset(test,r=1.9, closed=true, check_valid=true,quality=1));  // This fails
%down(.1)polygon(test);
*/

// Example 4

/*
$fn=12;
star = star(5, r=22, ir=13);
rounded_star = roundcorners(array_zip(star, flatten(replist([.5,0],5))), curve="circle", type="cut");

polygon(pathoffset(rounded_star,r=8,closed=true,check_valid=true));
right(15)polygon(pathoffset(rounded_star,delta=8,closed=true,check_valid=true));
*/


// Example 5

/*
star = star(5, r=22, ir=13);
rounded_star = roundcorners(array_zip(star, flatten(replist([.5,0],5))), curve="circle", type="cut", $fn=12);


rounded_extrude(rounded_star, height=20, r1=4, r2=1, steps=15, edge1="round", edge2="round");

right(45) rounded_extrude(rounded_star, height=20, r1=-4, r2=1, steps=15, edge1="round", edge2="round");

// Check out the rounded corners on the chamfer
right(90) rounded_extrude(rounded_star, height=20, r1=4, r2=4, steps=15, edge1="teardrop", edge2="chamfer");

left(45)
difference(){
  rounded_extrude((star), height=20, r1=4, r2=1, steps=15, edge1="sharp", edge2="sharp");
  //up(2)rounded_extrude(pathoffset(reverse(rounded_star),r=-.5,closed=true), height=18, r1=1, r2=-1, steps=15,extra2=1);
  up(2)rounded_extrude(pathoffset(reverse(star),r=-2,closed=true), height=18, r1=7, r2=-1, steps=15,extra2=1);
}

*/

// Example 6

/*
path = 2*[for(theta=[0:360]) [theta/75,sin(theta)]];
color("red")stroke(path,thickness=.1);
stroke(pathoffset(path, r=1.75), thickness=.1);
*/

Include constants by default (and compiler?)

I don't know if its an OpenSCAD limitation, but do we have to include constants.scad when all of the used modules already do that?

include <BOSL/constants.scad>
use <BOSL/transforms.scad>
use <BOSL/shapes.scad>
use <BOSL/threading.scad>
use <BOSL/masks.scad>

Also, what about a sort of "compiler"? For example when uploading to Thingiverse we can't use the Customizer function as they don't have the BOSL library on their servers, so could we have a compiler to pull in all of the functions and append it to your main scad file (and remove the use/include statements?)

More range index bugs

I started reading through arrays.scad for other places where indexing bugs appear due to the idiotic behavior of ranges that end before they start. It looks like the following need fixing:

list_range (has two ranges that need a step of 1 added)
list_remove (has one range that needs the fix)
list_pad (but I already proposed a bigger change to that one)
list_trim can be cleaned up a bit once the 1 is added, and
enumerate can also have the test for the empty list removed after the fix
pair and pair_wrap both need the fix, failing horribly on the empty list with the return [[undef,undef],[undef,undef]]
zip needs the fix in two places
transpose needs the fix as well, and then you can delete the check for empty list. (Right now you get a bad result from tranpose([[],[]]) all though one could debate what the answer should be, probably not [[undef, undef], [undef, undef]]

You must not use list_insert much---it's completely broken due to typo: slide->slice.
flatten can be rewritten to use each. Did I post that as an issue already somewhere?

Anyway, this behavior is pretty annoying, but at least adding the step of 1 in the range provides a quick and easy fix. The main problem is tracking down all the cases.

superformula broken (a,b not passed through)

I tried to actually use this for a real project and found that the superformula is broken. The a and b parameters aren't passed through. I also thought scale was a pointless argument since you can just rescale later. But a "radius" factor seems advantageous, since it's hard to predict the maximum size of the shape. It also seems desirable to have default values that are a little more relative (e.g. m2=m1, n3=n2). So I tweaked the code and added some examples. (One example is not enough!) Probably more examples are needed, but I'm not sure what. Also some people call this the supershape, which seems better than superformula_shape.

Also it's not clear that anchor does something useful, since it doesn't (generally) put a point that is on the shape at the anchor point. The old anchor code didn't respect the change in scale, so I fixed that, but the problem still remains.

// Function&Module: supershape()
// Usage:
//   supershape(step,[m1],[m2],[n1],[n2],[n3],[a],[b],[r|d]);
// Description:
//   When called as a function, returns a 2D path for the outline of the [Superformula](https://en.wikipedia.org/wiki/Superformula) shape.
//   When called as a module, creates a 2D [Superformula](https://en.wikipedia.org/wiki/Superformula) shape.
// Arguments:
//   step = The angle step size for sampling the superformula shape.  Smaller steps are slower but more accurate.
//   m1 = The m1 argument for the superformula. Default: 4. 
//   m2 = The m2 argument for the superformula. Default: m1.
//   n1 = The n1 argument for the superformula. Default: 1.
//   n2 = The n2 argument for the superformula. Default: n1.
//   n3 = The n3 argument for the superformula. Default: n2.
//   a = The a argument for the superformula.  Default: 1.
//   b = The b argument for the superformula.  Default: a.
//   r = Radius of the shape.  Scale shape to fit in a circle of radius r.
//   d = Diameter of the shape.  Scale shape to fit in a circle of diameter d.
//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#anchor).  Default: `CENTER`
//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#spin).  Default: `0`
// Example(2D):
//   supershape(step=0.5,m1=16,m2=16,n1=0.5,n2=0.5,n3=16,r=50);
// Example(2D): Called as Function
//   stroke(close=true, supershape(step=0.5,m1=16,m2=16,n1=0.5,n2=0.5,n3=16,d=100));
// Examples2(2D):
//   for(n=[2:5]) right(2.5*(n-2)) supershape(m1=4,m2=4,n1=n,a=1,b=2);  // Superellipses
//   m=[2,3,5,7]; for(i=[0:3]) right(2.5*i) supershape(.5,m1=m[i],n1=1);
//   m=[6,8,10,12]; for(i=[0:3]) right(2.7*i) supershape(.5,m1=m[i],n1=1,b=1.5);  // m should be even
//   m=[1,2,3,5]; for(i=[0:3]) fwd(1.5*i) supershape(m1=m[i],n1=0.4);
//   supershape(m1=5, n1=4, n2=1); right(2.5) supershape(m1=5, n1=40, n2=10);
//   m=[2,3,5,7]; for(i=[0:3]) right(2.5*i) supershape(m1=m[i], n1=60, n2=55, n3=30);
//   n=[0.5,0.2,0.1,0.02]; for(i=[0:3]) right(2.5*i) supershape(m1=5,n1=n[i], n2=1.7);
//   supershape(m1=2, n1=1, n2=4, n3=8);
//   supershape(m1=7, n1=2, n2=8, n3=4);
//   supershape(m1=7, n1=3, n2=4, n3=17);
//   supershape(m1=4, n1=1/2, n2=1/2, n3=4);
//   supershape(m1=4, n1=4.0,n2=16, n3=1.5, a=0.9, b=9);
//   for(i=[1:4]) right(3*i) supershape(m1=i, m2=3*i, n1=2);  
//   m=[4,6,10]; for(i=[0:2]) right(i*5) supershape(m1=m[i], n1=12, n2=8, n3=5, a=2.7);
function supershape(step=0.5,m1=4,m2=undef,n1=1,n2=undef,n3=undef,a=1,b=undef,r=undef,d=undef,anchor=CENTER, spin=0) =
	let(
                r = get_radius(r=r,d=d,dflt=undef),
                m2 = is_def(m2) ? m2 : m1,
                n2 = is_def(n2) ? n2 : n1,
                n3 = is_def(n3) ? n3 : n2,
                b = is_def(b) ? b : a,
		steps = ceil(360/step),
		step = 360/steps,
		angs = [for (i = [0:steps-1]) step*i],
		rads = [for (theta = angs) _superformula(theta=theta,m1=m1,m2=m2,n1=n1,n2=n2,n3=n3,a=a,b=b)],
                scale = is_def(r) ? r/max(rads) : 1,
		path = [for (i = [0:steps-1]) let(a=angs[i]) scale*rads[i]*[cos(a), sin(a)]]
	) rot(spin, p=move(-scale*max(rads)*normalize(anchor), p=path));

module supershape(step=0.5,m1=4,m2=undef,n1,n2=undef,n3=undef,a=1,b=undef, r=undef, d=undef, anchor=CENTER, spin=0)
	polygon(supershape(step=step,m1=m1,m2=m2,n1=n1,n2=n2,n3=n3,a=a,b=b, r=r,d=d, anchor=anchor, spin=spin));

matrix3_mult? Isn't this just a product (of anything)?

Isn't this just using the built-in * operator to multiply a list of items?

I think it should be called prod() (matlab name for same thing, like sum()), or maybe product() if you think prod() is too short and the optional parameter m deleted, since
product(list)*m is perfectly fine. And in fact I'm not sure if the existing code does that or m*product(list), so the more explicit notation is better. (I suppose what I really mean is don't expose the accumulator that you need for tail recursion as part of the interface.)

You can do the product "backwards" and it will be fine for everything else, since matrices are the only non-commutative object that can be multiplied.

replace any_defined with num_defined, remove remove_undefs, add all_defined

It seems like remove_undefs is a weird operation, not something I could think of a reason to use. You use it to define first_defined. Does any other application exist? I think num_undefs is more useful (and can be used to achieve the result of any_defined with a simple check). I've found it nice to count defined entries for parameter checking and having num_defined is nice for that, and better than len(remove_undefs()). So here's a tail recursive version of first_defined:

function first_defined(v,ind=0) = ind<len(v) && is_undef(v[ind]) ? first_defined(v,ind+1) : (ind==len(v) ? undef : v[ind]);

Not sure that's necessary, but it can handle long lists and may as well be robust when possible. (I can't think of a use case other than the parameter selection one in the code, which obviously doesn't involve long lists.)

It might also be useful to have all_defined() for checking validity of a calculation, for example.

function all_defined(v) = is_list(v) ? _all_defined_list(v) : !is_undef(v);
function _all_defined_list(v,ind=0) = ind<len(v) && all_defined(v[ind]) ? _all_defined_list(v,ind+1) : (ind==len(v) ? true : false);

I needed something like this in pathoffset, though not so sophisticated. (I didn't need to check inside list members.) But it seems like that's the right way to define it, so [[3,4],[5,6],[undef,undef],[5,6]] tests as being bad. And this one needs to be tail recursive to handle potential long inputs. I found when I tested speed with _makefaces_recursive in my path offset code that making it tail recursive didn't affect speed but did prevent errors on long inputs.

add epsilon to line_segment_intersection and segment_intersection

It's funny that I didn't run into this problem until recently, but it seems like we need to add an epsilon to the endpoint checks, because my code is failing to find an intersection at the endpoints.

So

    int[1]<0 || int[1]>1 || int[2]<0 || int[2]>1 ? undef : int[0];

becomes

    int[1]<-eps || int[1]>1+1ps || int[2]<-eps || int[2]>1+eps ? undef : int[0];

and

     int[2]<0 || int[2]>1 ? undef : int[0];

becomes

     int[2]<-eps || int[2]>1+eps ? undef : int[0];

uts screws

You suggested I should supply code for UTS screws. Here is my attempt to do so. It parses a screw spec and produces a structure with screw data. I made a head-drawing routine but didn't try to figure out how to draw the threaded shaft, or to put a recess into the head.

Note also that there's a bunch of string stuff that belongs in a strings library. I wonder if we should try to get permission to use the regular expression parser from the STring Theory library. (It's GPL.)

[Code elided as it was merged]

replace (?) triangle_area2d with polygon_area

function polygon_area(vertices) =
     0.5*sum([for(i=[0:len(vertices)-1]) det2(select(vertices,i,i+1))]);

Note if polygon self intersects then some areas have positive area, some have negative, and they are summed.

This function is slower than triangle_area2d, so maybe it's worth keeping both? (This takes about twice as long.) I'm not sure how much of a difference it makes in the run time of hull() for example. It took 9s to do 100k computations with polygon_area and 4s with triangle_area2d. I don't know how many times hull() calls triangle_area2d().

I do think that triangle_area2d should be renamed triangle_area if it is retained, unless you plan to add triangle_area3d.

function triangle_area3d(x1,x2,x3) = 0.5*norm(cross(x3-x1,x3-x2));

I would prefer to have one triangle_area function that checks the dimension. The 3d version is not signed.

add line intersection and determinants

Here's code that I've been using in pathoffset that belongs somewhere. I went ahead and implemented det3 even though I don't have an application for it right now just so it's there. But I'm not going to do det4 unless someone asks for it. (The code is either a kind of recursive mess or it's four copies of the det3 code, but each copy slightly different.)

// Line intersection from two segments

// This function returns [p,t,u] where p is the intersection point of
// the lines defined by the two segments, t is the bezier parameter
// for the intersection point on s1 and u is the bezier parameter for
// the intersection point on s2.  The bezier parameter runs over [0,1]
// for each segment, so if it is in this range, then the intersection
// lies on the segment.  Otherwise it lies somewhere on the extension
// of the segment.

function _general_line_intersection(s1,s2) =
  let(  denominator = det2([s1[0],s2[0]]-[s1[1],s2[1]]),
        t=det2([s1[0],s2[0]]-s2)/denominator,
        u=det2([s1[0],s1[0]]-[s1[1],s2[1]])/denominator)
        [denominator==0 ? undef : s1[0]+t*(s1[1]-s1[0]),t,u];

// Returns the intersection point of the lines defined by two segments, or undef
// if the lines are parallel.  
function line_intersection(s1,s2) = let( int = _general_line_intersection(s1,s2)) int[0];

// Returns the intersection point of two segments and undef if they do not intersect
function segment_intersection(s1,s2) = let( int = _general_line_intersection(s1,s2))
        int[1]<0 || int[1]>1 || int[2]<0 || int[2]>1 ? undef : int[0];

// Returns the intersection point of a line defined by the first segment with a segment.
// If the segment doesn't intersect the line it returns undef
function line_segment_intersection(line,segment) = let( int = _general_line_intersection(line,segment))
         int[2]<0 || int[2]>1 ? undef : int[0];
        

// Return true if simple polygon is in clockwise order, false otherwise.
// Results for complex (self-intersecting) polygon are indeterminate
function polygon_clockwise(path) =
  let( 
       minx = min(array_subindex(path,0)),
       lowind = search(minx, path, 0, 0),
       lowpts = select(path, lowind),
       miny = min(array_subindex(lowpts, 1)),
       extreme_sub = search(miny, lowpts, 1, 1)[0],
       extreme = select(lowind,extreme_sub)
     )
  det2(  [select(path,extreme+1)-path[extreme], select(path, extreme-1)-path[extreme]])<0;

// determinant of 2x2 matrix
function det2(M) = M[0][0] * M[1][1] - M[0][1]*M[1][0];

function det3(M) =  M[0][0] * (M[1][1]*M[2][2]-M[2][1]*M[1][2])
                  - M[1][0] * (M[0][1]*M[2][2]-M[2][1]*M[0][2])
                 + M[2][0] * (M[0][1]*M[1][2]-M[1][1]*M[0][2]);

// Returns the segment normal to the input segment
//   normal_segment(p1,p2)
//   normal_segment([p1,p2])
// It returns the segment whose first point is the center of the input and whose second point extends left 
// The length of the segment is half the length of the input.  
function normal_segment(p1,p2) = let(center = (p1+p2)/2)
     [center, center + norm(p1-p2)/2 * normalize([p1.y-p2.y,p2.x-p1.x])];

I'm a bit unsure about normal_segment. It's used by arc(). But maybe it needs to be designed differently to be less particular.

add strings functions

I went ahead and pulled out the strings stuff from my screws library and documented it so it should be ready to go. Extended a couple of the functions a bit.

// substr() by Nathanaël Jourdane, Creative Commons CC-BY (Attribution)

// Function: substr()
// Usage:
//   substr(str, [pos], [len])
// Description:
//   Returns a substring from a string start at position `pos` with length `len`.
// Arguments:
//   str = string to operate on
//   pos = starting index of substring.  Default: 0
//   len = length of substring, or -1 for the rest of the string.  Default: -1
// Example:
//   substr("abcdefg",3,3);    // Returns "def"
//   substr("abcdefg",2);      // Returns "cdefg"
//   substr("abcdefg",len=3);  // Returns "abc"
function substr(str, pos=0, len=-1, substr="") =
        assert(len>=-1,"illegal value of len")
	len == 0 || pos>=len(str) ? substr :
	len == -1 ? substr(str, pos, len(str)-pos, substr) :
	substr(str, pos+1, len-1, str(substr, str[pos]));

// Function: strcat()
// Usage:
//   strcat(list, [sep])
// Description:
//   Returns the concatenation of a list of strings, optionally with a
//   separator string inserted between each string on the list.
// Arguments:
//   list = list of strings to concatenate
//   sep = separator string to insert.  Default: ""
// Example:
//   strcat(["abc","def","ghi"]);        // Returns "abcdefghi"
//   strcat(["abc","def","ghi"], " + ");  // Returns "abc + def + ghi"
function strcat(list,sep="",_i=0, _result="") =
    _i >= len(list)-1 ? (_i==len(list) ? _result : str(_result,list[_i])) :
      strcat(list,sep,_i+1,str(_result,list[_i],sep));

// Function: downcase()
// Usage:
//   downcase(str)
// Description:
//   Returns the string with the standard ASCII upper case letters A-Z replaced
//   by their lower case versions.
// Arguments:
//   str = string to convert
// Example:
//   downcase("ABCdef");   // Returns "abcdef"
function downcase(str) =
   strcat([for(char=str) let(code=ord(char)) code>=65 && code<=90 ? chr(code+32) : char]);

// Function: atoi()
// Usage:
//   atoi(str, [base])
// Description:
//   Converts a string into an integer with any base up to 16.  Returns NaN if 
//   conversion fails.  Digits above 9 are represented using letters A-F in either
//   upper case or lower case.  
// Arguments:
//   str = string to convert
//   base = base for conversion, from 2-16.  Default: 10
// Example:
//   atoi("349");        // Returns 349
//   atoi("-37");        // Returns -37
//   atoi("+97");        // Returns 97
//   atoi("43.9");       // Returns nan
//   atoi("1011010",2);  // Returns 90
//   atoi("13",2);       // Returns nan
//   atoi("dead",16);    // Returns 57005
//   atoi("CEDE", 16);   // Returns 52958
//   atoi("");           // Returns 0
function atoi(str,base=10) =
    len(str)==0 ? 0 : 
    let(str=downcase(str))
    str[0] == "-" ? -_atoi_recurse(substr(str,1),base,len(str)-2) :
    str[0] == "+" ?  _atoi_recurse(substr(str,1),base,len(str)-2) :
    _atoi_recurse(str,base,len(str)-1);

function _atoi_recurse(str,base,i) =
    let( digit = search(str[i],"0123456789abcdef"),
         last_digit = digit == [] || digit[0] >= base ? (0/0) : digit[0])
    i==0 ? last_digit : 
        _atoi_recurse(str,base,i-1)*base + last_digit;

// Function: atof()
// Usage:
//   atof(str)
// Description:
//   Converts a string to a floating point number.  Returns NaN if the
//   conversion fails.
// Arguments:
//   str = string to convert
// Example:
//   atof("44");       // Returns 44
//   atof("3.4");      // Returns 3.4
//   atof("-99.3332"); // Returns -99.3332
//   atof("3.483e2");  // Returns 348.3
//   atof("-44.9E2");  // Returns -4490
//   atof("7.342e-4"); // Returns 0.0007342
//   atof("");         // Returns 0
function atof(str) =
     len(str) == 0 ? 0 :
     in_list(str[1], ["+","-"]) ? (0/0) : // Don't allow --3, or +-3
     str[0]=="-" ? -atof(substr(str,1)) :
     str[0]=="+" ?  atof(substr(str,1)) :
     let(esplit = strsplit(str,"eE") )
     len(esplit)==2 ? atof(esplit[0]) * pow(10,atoi(esplit[1])) :
     let( dsplit = strsplit(str,["."]))
     atoi(dsplit[0])+atoi(dsplit[1])/pow(10,len(dsplit[1]));

// Function: fractof()
// Usage:
//   fractof(str)
// Description:
//   Converts a string fraction, two integers separated by a "/" character, to a floating point number.
// Arguments:
//   str = string to convert
// Example:
//   fractof("3/4");     // Returns 0.75
//   fractof("-77/9");   // Returns -8.55556
//   fractof("+1/3");    // Returns 0.33333
//   fractof("19");      // Returns 19
//   fractof("");        // Returns 0
//   fractof("3/0");     // Returns inf
//   fractof("0/0");     // Returns nan
function fractof(str) =
    let( num = strsplit(str,"/"))
    len(num)==1 ? atoi(num[0]) :
    len(num)==2 ? atoi(num[0])/atoi(num[1]) :
    (0/0);


// Function: strsplit()
// Usage:
//   strsplit(str, sep, [keep_nulls])
// Description:
//   Breaks an input string into substrings using a separator or list of separators.  If keep_nulls is true
//   then two sequential separator characters produce an empty string in the output list.  If keep_nulls is false
//   then no empty strings are included in the output list.
//
//   If sep is a single string then each character in sep is treated as a delimiting character and the input string is
//   split at every delimiting character.  Empty strings can occur whenever two delimiting characters are sequential.
//   If sep is a list of strings then the input string is split sequentially using each string from the list in order. 
//   If keep_nulls is true then the output will have length equal to `len(sep)+1`, possibly with trailing null strings
//   if the string runs out before the separator list.  
// Arguments
//   str = string to split
//   sep = a string or list of strings to use for the separator
//   keep_nulls = boolean value indicating whether to keep null strings in the output list.  Default: true
// Example:
//   strsplit("abc+def-qrs*iop","*-+");     // Returns ["abc", "def", "qrs", "iop"]
//   strsplit("abc+*def---qrs**iop+","*-+");// Returns ["abc", "", "def", "", "", "qrs", "", "iop", ""]
//   strsplit("abc      def"," ");          // Returns ["abc", "", "", "", "", "", "def"]
//   strsplit("abc      def"," ",keep_nulls=false);  // Returns ["abc", "def"]
//   strsplit("abc+def-qrs*iop",["+","-","*"]);     // Returns ["abc", "def", "qrs", "iop"]
//   strsplit("abc+def-qrs*iop",["-","+","*"]);     // Returns ["abc+def", "qrs*iop", "", ""]
function strsplit(str,sep,keep_nulls=true) =
   !keep_nulls ?echo("removing") _remove_empty_strs(strsplit(str,sep,keep_nulls=true)) :
   is_list(sep) ? strsplit_recurse(str,sep,i=0,result=[]) :
   let( cutpts = concat([-1],sort(flatten(search(sep, str,0))),[len(str)]))
   [for(i=[0:len(cutpts)-2]) substr(str,cutpts[i]+1,cutpts[i+1]-cutpts[i]-1)];

function strsplit_recurse(str,sep,i,result) =
   i == len(sep) ? concat(result,[str]) :
    let( pos = search(sep[i], str),
         end = pos==[] ? len(str) : pos[0]
      )
    strsplit_recurse(substr(str,end+1), sep, i+1,
                    concat(result, [substr(str,0,end)]));
                    
function _remove_empty_strs(list) =
    list_remove(list, search([""], list,0)[0]);

grid2d() broken

When I run the first example:

grid2d(size=50, spacing=10, stagger=false) {cylinder(d=10, h=1);}

I get

ERROR: Assertion 'false' failed: "Bad arguments." in file lib/BOSL2/vectors.scad, line 120
TRACE: called by 'vector_angle', in file lib/BOSL2/coords.scad, line 155.
TRACE: called by 'rotate_points3d', in file lib/BOSL2/attachments.scad, line 156.
TRACE: called by 'find_anchor', in file lib/BOSL2/attachments.scad, line 253.
TRACE: called by 'orient_and_anchor', in file lib/BOSL2/transforms.scad, line 1056.
TRACE: called by 'if', in file lib/BOSL2/transforms.scad, line 1055.
TRACE: called by 'if', in file lib/BOSL2/transforms.scad, line 1036.
TRACE: called by 'grid2d', in file lib/BOSL2/transforms.scad, line 1042.
TRACE: called by 'if', in file lib/BOSL2/transforms.scad, line 1038.
TRACE: called by 'if', in file lib/BOSL2/transforms.scad, line 1036.
TRACE: called by 'grid2d', in file test47.scad, line 16.

checks from math, vector, affine

I looked through more of the files for indexing errors.

In math.scad, vmul and vdiv need the fix.

For deltas the base case is wrong. It will be right if you fix pair and take out the test for length<2. (Diff of a single value should be the empty list...or I guess you could argue for undef, but that seems more likely to break something somehow.)

ident needs the fix. (ident(0) gives a 2x2 matrix.)

Another observation is that affine3d_apply is inefficient. I'm not sure if it matters, but if the matrix product is m1m2m3...m100 point it's much more efficient to group the computation as m1*(m2*(m3*(...(m100point)))) because then all the products are matrix * vector. The way you're doing it the products are matrix*matrix. This would only matter, I think for hundreds or maybe thousands of matrices multiplied together.

Attachments documentation

I'm trying to figure out how to use the new attachments stuff and confused about it. I have constructed a shape that is an extruded octagon with a rounded cylinder subtracted. I want to add a dovetail slot off center at one edge of the octagon. To do this (because of how my dovetail code works) need to subtract a cube and then add a dovetail part. How do I do this using the attachments? That is, I need to mark the location on the model where the thing goes (using the anchor function?) and then do I also create anchors for the cube I want to subtract and the dovetail slot part I want to add in? Then I somehow use orient_and_anchor?

replist fails for n=0

When n=0 replist returns two copies of the input list.

I don't know what they were thinking when they made a range [n:n-k] be the same as [n-k:n]. I almost wonder if we should just go through the code and add a step=1 for every range to prevent more of these bugs cropping up. The fix:

function replist(val, n, i=0) =
	is_num(n)? [for(j=[1:1:n]) val] :
	(i>=len(n))? val :
	[for (j=[1:1:n[i]]) replist(val, n, i+1)];

add list_set and fast version of list_remove

// Function: list_set()
//
// list_set(indices, values, list, dftl, minlen)
// Takes the input list and returns a new list such that
//    list[indices[i]] = values[i]
// for all of the (index,value) pairs supplied.  If you supply indices
// that are beyond the length of the list then the list is extended
// and filled in with the dflt value.
//
// If you set minlen then the list is lengthed, if necessary, by padding
// with dflt to that length.  
// 
// The `indices` list can be in any order but run time will be (much) faster
// for long lists if it is already sorted.  Reptitions are not allowed.  
// 
function list_set(indices,values,list=[],dflt=0,minlen=0) =
    !is_list(indices) ? list_set(list,[indices],[values],dflt) :
    assert(len(indices)==len(values),"Index list and value list must have the same length")
    let( sortind = list_increasing(indices) ? list_range(len(indices)) : sortidx(indices),
         lastind = indices[select(sortind,-1)]
    )
    concat([for(j=[0:1:indices[sortind[0]]-1]) j>=len(list) ? dflt : list[j]], [values[sortind[0]]], 
          [for(i=[1:1:len(sortind)-1])   
                                        each
                                          assert(indices[sortind[i]]!=indices[sortind[i-1]],"Repeated index")
                                          concat(
                                            [for(j=[1+indices[sortind[i-1]]:1:indices[sortind[i]]-1]) j>=len(list) ? dflt : list[j]],
                                            [values[sortind[i]]]
                                         )
          ],
          slice(list,1+lastind, len(list)),
          replist(dflt, minlen-lastind-1)
    );



function new_list_remove(list,elements) =
    !is_list(elements) ? list_remove(list,[elements]) :
    let( sortind = list_increasing(elements) ? list_range(len(elements)) : sortidx(elements),
         lastind = elements[select(sortind,-1)]
    )
    assert(lastind<len(list),"Element index beyond list end")
    concat(slice(list, 0, elements[sortind[0]]),
          [for(i=[1:1:len(sortind)-1]) each slice(list,1+elements[sortind[i-1]], elements[sortind[i]])],
          slice(list,1+lastind, len(list))
    );



// True if the list is (non-strictly) increasing
function list_increasing(list,ind=0) = ind < len(list)-1 && list[ind]<=list[ind+1] ? list_increasing(list,ind+1) :
                                       (ind>=len(list)-1 ? true : false);


// True if the list is (non-strictly) decreasing
function list_decreasing(list,ind=0) = ind < len(list)-1 && list[ind]>=list[ind+1] ? list_increasing(list,ind+1) :
                                       (ind>=len(list)-1 ? true : false);

Negative rounding and chamfers

cuboid() and cyl(), can do edge rounding and chamfering, but it would be useful to be able to make "negative" roundings and chamfers to make flanges at the top and bottom to make masks for rounded or chamfered holes.

more rounding flexibility for prismoid

I needed to round some but not all of the edges of a prismoid...but also some top edges. I ended up having to intersection with a cuboid to round the top and then union with an extra prismoid to unround the edges I didn't want rounded. More flexibility (as with cuboid) would be nice.

is_def vanished

So I know it's not necessary, but I got used to it. Why did you remove it?

[BUG] Fix assemble_path_fragments() to minimize assembled path length

Describe the bug
The function assemble_path_fragments(), which is used to reassemble path fragments into final paths has a bug where it can mis-assemble self-crossing paths. This function needs to be rewritten to minimize assembled closed path lengths.

Code To Reproduce Bug

rgn = [circle(d=100), square([20,60], center=true), square([60,20], center=true)];
rgn2 = exclusive_or([for (p=rgn) [p]]); 
for (p=rgn2) stroke(p);
for (p=rgn2) echo("rgn2", p=p);

Expected behavior
The returned region should be a collection of the shortest closed paths, or at least a collection of paths that do not self-intersect.

Screenshots
Screen Shot 2019-06-26 at 7 05 54 PM

Always have diameter and radius

Some functions only take a diameter and not a radius or vice versa, in v2 can we take both by default please as its easier than having to look up which ones take which parameters.

add rand_int() and shuffle()

Because I needed to test other stuff I wrote these. It's a bit tricky to decide if shuffle() is uniform, but I think it is. The obvious way to write shuffle, building one entry at a time, is drastically slower.



// Function: shuffle(list)
//
// Shuffles the input list into random order.
//
function shuffle(list) =
      len(list)<=1 ? list :
      let (
             rval = rands(0,1,len(list)),
             left = [for (i=[0:len(list)-1]) if (rval[i]<0.5) list[i]],
             right = [for (i=[0:len(list)-1]) if (rval[i]>=0.5) list[i]]
          )
       concat(shuffle(left), shuffle(right));

// Function: rand_int(min,max,N,seed)
//
// Return list of random integers in the listed range (inclusive)
function rand_int(min,max,N,seed=undef) =
  assert(max >= min, "Max value cannot be smaller than min")
  let (rvect = is_def(seed) ? rands(min,max+1,N,seed) : rands(min,max+1,N))
  [for(entry = rvect) floor(entry)];

It appears that the seed you pass affects only the one rands() call. There's no simple way to get a series of random values from a desired seed. (I guess you have to get all the random data you want all at once and then parcel it out.) Ug. That's why there's no seed option to shuffle(). I have no way to predict how many random values shuffle() will need, so it can't be done ahead.

stroke: change close argument to closed

I use the "closed" argument in several functions, so stroke is inconsistent. I think "closed" makes more sense (describing the input curve as being closed) rather than "close" the verb form, which doesn't always apply.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.