1//////////////////////////////////////////////////////////////////////
   2// LibFile: skin.scad
   3//   This file provides functions and modules that construct shapes from a list of cross sections.
   4//   In the case of skin() you specify each cross sectional shape yourself, and the number of
   5//   points can vary.  The various forms of sweep use a fixed shape, which may follow a path, or
   6//   be transformed in other ways to produce the list of cross sections.  In all cases it is the
   7//   user's responsibility to avoid creating a self-intersecting shape, which will produce
   8//   cryptic CGAL errors.  This file was inspired by list-comprehension-demos skin():
   9//   - https://github.com/openscad/list-comprehension-demos/blob/master/skin.scad
  10// Includes:
  11//   include <BOSL2/std.scad>
  12// FileGroup: Advanced Modeling
  13// FileSummary: Construct 3D shapes from 2D cross sections of the desired shape.
  14// FileFootnotes: STD=Included in std.scad
  15//////////////////////////////////////////////////////////////////////
  16
  17
  18// Section: Skin and sweep
  19
  20// Function&Module: skin()
  21// Usage: As module:
  22//   skin(profiles, slices, [z=], [refine=], [method=], [sampling=], [caps=], [closed=], [style=], [convexity=], [anchor=],[cp=],[spin=],[orient=],[atype=]) [ATTACHMENTS];
  23// Usage: As function:
  24//   vnf = skin(profiles, slices, [z=], [refine=], [method=], [sampling=], [caps=], [closed=], [style=], [anchor=],[cp=],[spin=],[orient=],[atype=]);
  25// Description:
  26//   Given a list of two or more path `profiles` in 3d space, produces faces to skin a surface between
  27//   the profiles.  Optionally the first and last profiles can have endcaps, or the first and last profiles
  28//   can be connected together.  Each profile should be roughly planar, but some variation is allowed.
  29//   Each profile must rotate in the same clockwise direction.  If called as a function, returns a
  30//   [VNF structure](vnf.scad) `[VERTICES, FACES]`.  If called as a module, creates a polyhedron
  31//    of the skinned profiles.
  32//   .
  33//   The profiles can be specified either as a list of 3d curves or they can be specified as
  34//   2d curves with heights given in the `z` parameter.  It is your responsibility to ensure
  35//   that the resulting polyhedron is free from self-intersections, which would make it invalid
  36//   and can result in cryptic CGAL errors upon rendering with a second object present, even though the polyhedron appears
  37//   OK during preview or when rendered by itself.
  38//   .
  39//   For this operation to be well-defined, the profiles must all have the same vertex count and
  40//   we must assume that profiles are aligned so that vertex `i` links to vertex `i` on all polygons.
  41//   Many interesting cases do not comply with this restriction.  Two basic methods can handle
  42//   these cases: either subdivide edges (insert additional points along edges)
  43//   or duplicate vertcies (insert edges of length 0) so that both polygons have
  44//   the same number of points.
  45//   Duplicating vertices allows two distinct points in one polygon to connect to a single point
  46//   in the other one, creating
  47//   triangular faces.  You can adjust non-matching polygons yourself
  48//   either by resampling them using {{subdivide_path()}} or by duplicating vertices using
  49//   `repeat_entries`.  It is OK to pass a polygon that has the same vertex repeated, such as
  50//   a square with 5 points (two of which are identical), so that it can match up to a pentagon.
  51//   Such a combination would create a triangular face at the location of the duplicated vertex.
  52//   Alternatively, `skin` provides methods (described below) for inserting additional vertices
  53//   automatically to make incompatible paths match.
  54//   .
  55//   In order for skinned surfaces to look good it is usually necessary to use a fine sampling of
  56//   points on all of the profiles, and a large number of extra interpolated slices between the
  57//   profiles that you specify.  It is generally best if the triangles forming your polyhedron
  58//   are approximately equilateral.  The `slices` parameter specifies the number of slices to insert
  59//   between each pair of profiles, either a scalar to insert the same number everywhere, or a vector
  60//   to insert a different number between each pair.
  61//   .
  62//   Resampling may occur, depending on the `method` parameter, to make profiles compatible.
  63//   To force (possibly additional) resampling of the profiles to increase the point density you can set `refine=N`, which
  64//   will multiply the number of points on your profile by `N`.  You can choose between two resampling
  65//   schemes using the `sampling` option, which you can set to `"length"` or `"segment"`.
  66//   The length resampling method resamples proportional to length.
  67//   The segment method divides each segment of a profile into the same number of points.
  68//   This means that if you refine a profile with the "segment" method you will get N points
  69//   on each edge, but if you refine a profile with the "length" method you will get new points
  70//   distributed around the profile based on length, so small segments will get fewer new points than longer ones.
  71//   A uniform division may be impossible, in which case the code computes an approximation, which may result
  72//   in arbitrary distribution of extra points.  See {{subdivide_path()}} for more details.
  73//   Note that when dealing with continuous curves it is always better to adjust the
  74//   sampling in your code to generate the desired sampling rather than using the `refine` argument.
  75//   .
  76//   You can choose from five methods for specifying alignment for incommensurate profiles.
  77//   The available methods are `"distance"`, `"fast_distance"`, `"tangent"`, `"direct"` and `"reindex"`.
  78//   It is useful to distinguish between continuous curves like a circle and discrete profiles
  79//   like a hexagon or star, because the algorithms' suitability depend on this distinction.
  80//   .
  81//   The default method for aligning profiles is `method="direct"`.
  82//   If you simply supply a list of compatible profiles it will link them up
  83//   exactly as you have provided them.  You may find that profiles you want to connect define the
  84//   right shapes but the point lists don't start from points that you want aligned in your skinned
  85//   polyhedron.  You can correct this yourself using `reindex_polygon`, or you can use the "reindex"
  86//   method which will look for the index choice that will minimize the length of all of the edges
  87//   in the polyhedron&mdash;it will produce the least twisted possible result.  This algorithm has quadratic
  88//   run time so it can be slow with very large profiles.
  89//   .
  90//   When the profiles are incommensurate, the "direct" and "reindex" resample them to match.  As noted above,
  91//   for continuous input curves, it is better to generate your curves directly at the desired sample size,
  92//   but for mapping between a discrete profile like a hexagon and a circle, the hexagon must be resampled
  93//   to match the circle.  When you use "direct" or "reindex" the default `sampling` value is
  94//   of `sampling="length"` to approximate a uniform length sampling of the profile.  This will generally
  95//   produce the natural result for connecting two continuously sampled profiles or a continuous
  96//   profile and a polygonal one.  However depending on your particular case,
  97//   `sampling="segment"` may produce a more pleasing result.  These two approaches differ only when
  98//   the segments of your input profiles have unequal length.
  99//   .
 100//   The "distance", "fast_distance" and "tangent" methods work by duplicating vertices to create
 101//   triangular faces.  In the skined object created by two polygons, every vertex of a polygon must
 102//   have an edge that connects to some vertex on the other one.  If you connect two squares this can be
 103//   accomplished with four edges, but if you want to connect a square to a pentagon you must add a
 104//   fifth edge for the "extra" vertex on the pentagon.  You must now decide which vertex on the square to
 105//   connect the "extra" edge to.  How do you decide where to put that fifth edge?  The "distance" method answers this
 106//   question by using an optimization: it minimizes the total length of all the edges connecting
 107//   the two polygons.   This algorithm generally produces a good result when both profiles are discrete ones with
 108//   a small number of vertices.  It is computationally intensive (O(N^3)) and may be
 109//   slow on large inputs.  The resulting surfaces generally have curved faces, so be
 110//   sure to select a sufficiently large value for `slices` and `refine`.  Note that for
 111//   this method, `sampling` must be set to `"segment"`, and hence this is the default setting.
 112//   Using sampling by length would ignore the repeated vertices and ruin the alignment.
 113//   The "fast_distance" method restricts the optimization by assuming that an edge should connect
 114//   vertex 0 of the two polygons.  This reduces the run time to O(N^2) and makes
 115//   the method usable on profiles with more points if you take care to index the inputs to match.
 116//   .
 117//   The `"tangent"` method generally produces good results when
 118//   connecting a discrete polygon to a convex, finely sampled curve.  Given a polygon and a curve, consider one edge
 119//   on the polygon.  Find a plane passing through the edge that is tangent to the curve.  The endpoints of the edge and
 120//   the point of tangency define a triangular face in the output polyhedron.  If you work your way around the polygon
 121//   edges, you can establish a series of triangular faces in this way, with edges linking the polygon to the curve.
 122//   You can then complete the edge assignment by connecting all the edges in between the triangular faces together,
 123//   with many edges meeting at each polygon vertex.  The result is an alternation of flat triangular faces with conical
 124//   curves joining them.  Another way to think about it is that it splits the points on the curve up into groups and
 125//   connects all the points in one group to the same vertex on the polygon.
 126//   .
 127//   The "tangent" method may fail if the curved profile is non-convex, or doesn't have enough points to distinguish
 128//   all of the tangent points from each other.    The algorithm treats whichever input profile has fewer points as the polygon
 129//   and the other one as the curve.  Using `refine` with this method will have little effect on the model, so
 130//   you should do it only for agreement with other profiles, and these models are linear, so extra slices also
 131//   have no effect.  For best efficiency set `refine=1` and `slices=0`.  As with the "distance" method, refinement
 132//   must be done using the "segment" sampling scheme to preserve alignment across duplicated points.
 133//   Note that the "tangent" method produces similar results to the "distance" method on curved inputs.  If this
 134//   method fails due to concavity, "fast_distance" may be a good option.
 135//   .
 136//   It is possible to specify `method` and `refine` as arrays, but it is important to observe
 137//   matching rules when you do this.  If a pair of profiles is connected using "tangent" or "distance"
 138//   then the `refine` values for those two profiles must be equal.  If a profile is connected by
 139//   a vertex duplicating method on one side and a resampling method on the other side, then
 140//   `refine` must be set so that the resulting number of vertices matches the number that is
 141//   used for the resampled profiles.  The best way to avoid confusion is to ensure that the
 142//   profiles connected by "direct" or "realign" all have the same number of points and at the
 143//   transition, the refined number of points matches.
 144//   .
 145// Arguments:
 146//   profiles = list of 2d or 3d profiles to be skinned.  (If 2d must also give `z`.)
 147//   slices = scalar or vector number of slices to insert between each pair of profiles.  Set to zero to use only the profiles you provided.  Recommend starting with a value around 10.
 148//   ---
 149//   refine = resample profiles to this number of points per edge.  Can be a list to give a refinement for each profile.  Recommend using a value above 10 when using the "distance" or "fast_distance" methods.  Default: 1.
 150//   sampling = sampling method to use with "direct" and "reindex" methods.  Can be "length" or "segment".  Ignored if any profile pair uses either the "distance", "fast_distance", or "tangent" methods.  Default: "length".
 151//   closed = set to true to connect first and last profile (to make a torus).  Default: false
 152//   caps = true to create endcap faces when closed is false.  Can be a length 2 boolean array.  Default is true if closed is false.
 153//   method = method for connecting profiles, one of "distance", "fast_distance", "tangent", "direct" or "reindex".  Default: "direct".
 154//   z = array of height values for each profile if the profiles are 2d
 155//   convexity = convexity setting for use with polyhedron.  (module only) Default: 10
 156//   anchor = Translate so anchor point is at the origin.  Default: "origin"
 157//   spin = Rotate this many degrees around Z axis after anchor.  Default: 0
 158//   orient = Vector to rotate top towards after spin
 159//   atype = Select "hull" or "intersect" anchor types. Default: "hull"
 160//   cp = Centerpoint for determining "intersect" anchors or centering the shape.  Determintes the base of the anchor vector.  Can be "centroid", "mean", "box" or a 3D point.  Default: "centroid"
 161//   style = vnf_vertex_array style.  Default: "min_edge"
 162// Anchor Types:
 163//   "hull" = Anchors to the virtual convex hull of the shape.
 164//   "intersect" = Anchors to the surface of the shape.
 165// Example:
 166//   skin([octagon(4), circle($fn=70,r=2)], z=[0,3], slices=10);
 167// Example: Rotating the pentagon place the zero index at different locations, giving a twist
 168//   skin([rot(90,p=pentagon(4)), circle($fn=80,r=2)], z=[0,3], slices=10);
 169// Example: You can untwist it with the "reindex" method
 170//   skin([rot(90,p=pentagon(4)), circle($fn=80,r=2)], z=[0,3], slices=10, method="reindex");
 171// Example: Offsetting the starting edge connects to circles in an interesting way:
 172//   circ = circle($fn=80, r=3);
 173//   skin([circ, rot(110,p=circ)], z=[0,5], slices=20);
 174// Example(FlatSpin,VPD=20):
 175//   skin([ yrot(37,p=path3d(circle($fn=128, r=4))), path3d(square(3),3)], method="reindex",slices=10);
 176// Example(FlatSpin,VPD=16): Ellipses connected with twist
 177//   ellipse = xscale(2.5,p=circle($fn=80));
 178//   skin([ellipse, rot(45,p=ellipse)], z=[0,1.5], slices=10);
 179// Example(FlatSpin,VPD=16): Ellipses connected without a twist.  (Note ellipses stay in the same position: just the connecting edges are different.)
 180//   ellipse = xscale(2.5,p=circle($fn=80));
 181//   skin([ellipse, rot(45,p=ellipse)], z=[0,1.5], slices=10, method="reindex");
 182// Example(FlatSpin,VPD=500):
 183//   $fn=24;
 184//   skin([
 185//         yrot(0, p=yscale(2,p=path3d(circle(d=75)))),
 186//         [[40,0,100], [35,-15,100], [20,-30,100],[0,-40,100],[-40,0,100],[0,40,100],[20,30,100], [35,15,100]]
 187//   ],slices=10);
 188// Example(FlatSpin,VPD=600):
 189//   $fn=48;
 190//   skin([
 191//       for (b=[0,90]) [
 192//           for (a=[360:-360/$fn:0.01])
 193//               point3d(polar_to_xy((100+50*cos((a+b)*2))/2,a),b/90*100)
 194//       ]
 195//   ], slices=20);
 196// Example: Vaccum connector example from list-comprehension-demos
 197//   include <BOSL2/rounding.scad>
 198//   $fn=32;
 199//   base = round_corners(square([2,4],center=true), radius=0.5);
 200//   skin([
 201//       path3d(base,0),
 202//       path3d(base,2),
 203//       path3d(circle(r=0.5),3),
 204//       path3d(circle(r=0.5),4),
 205//       for(i=[0:2]) each [path3d(circle(r=0.6), i+4),
 206//                          path3d(circle(r=0.5), i+5)]
 207//   ],slices=0);
 208// Example: Vaccum nozzle example from list-comprehension-demos, using "length" sampling (the default)
 209//   xrot(90)down(1.5)
 210//   difference() {
 211//       skin(
 212//           [square([2,.2],center=true),
 213//            circle($fn=64,r=0.5)], z=[0,3],
 214//           slices=40,sampling="length",method="reindex");
 215//       skin(
 216//           [square([1.9,.1],center=true),
 217//            circle($fn=64,r=0.45)], z=[-.01,3.01],
 218//           slices=40,sampling="length",method="reindex");
 219//   }
 220// Example: Same thing with "segment" sampling
 221//   xrot(90)down(1.5)
 222//   difference() {
 223//       skin(
 224//           [square([2,.2],center=true),
 225//            circle($fn=64,r=0.5)], z=[0,3],
 226//           slices=40,sampling="segment",method="reindex");
 227//       skin(
 228//           [square([1.9,.1],center=true),
 229//            circle($fn=64,r=0.45)], z=[-.01,3.01],
 230//           slices=40,sampling="segment",method="reindex");
 231//   }
 232// Example: Forma Candle Holder (from list-comprehension-demos)
 233//   r = 50;
 234//   height = 140;
 235//   layers = 10;
 236//   wallthickness = 5;
 237//   holeradius = r - wallthickness;
 238//   difference() {
 239//       skin([for (i=[0:layers-1]) zrot(-30*i,p=path3d(hexagon(ir=r),i*height/layers))],slices=0);
 240//       up(height/layers) cylinder(r=holeradius, h=height);
 241//   }
 242// Example(FlatSpin,VPD=300): A box that is octagonal on the outside and circular on the inside
 243//   height = 45;
 244//   sub_base = octagon(d=71, rounding=2, $fn=128);
 245//   base = octagon(d=75, rounding=2, $fn=128);
 246//   interior = regular_ngon(n=len(base), d=60);
 247//   right_half()
 248//     skin([ sub_base, base, base, sub_base, interior], z=[0,2,height, height, 2], slices=0, refine=1, method="reindex");
 249// Example: Connecting a pentagon and circle with the "tangent" method produces large triangular faces and cone shaped corners.
 250//   skin([pentagon(4), circle($fn=80,r=2)], z=[0,3], slices=10, method="tangent");
 251// Example: rounding corners of a square.  Note that `$fn` makes the number of points constant, and avoiding the `rounding=0` case keeps everything simple.  In this case, the connections between profiles are linear, so there is no benefit to setting `slices` bigger than zero.
 252//   shapes = [for(i=[.01:.045:2])zrot(-i*180/2,cp=[-8,0,0],p=xrot(90,p=path3d(regular_ngon(n=4, side=4, rounding=i, $fn=64))))];
 253//   rotate(180) skin( shapes, slices=0);
 254// Example: Here's a simplified version of the above, with `i=0` included.  That first layer doesn't look good.
 255//   shapes = [for(i=[0:.2:1]) path3d(regular_ngon(n=4, side=4, rounding=i, $fn=32),i*5)];
 256//   skin(shapes, slices=0);
 257// Example: You can fix it by specifying "tangent" for the first method, but you still need "direct" for the rest.
 258//   shapes = [for(i=[0:.2:1]) path3d(regular_ngon(n=4, side=4, rounding=i, $fn=32),i*5)];
 259//   skin(shapes, slices=0, method=concat(["tangent"],repeat("direct",len(shapes)-2)));
 260// Example(FlatSpin,VPD=35): Connecting square to pentagon using "direct" method.
 261//   skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10);
 262// Example(FlatSpin,VPD=35): Connecting square to shifted pentagon using "direct" method.
 263//   skin([regular_ngon(n=4, r=4), right(4,p=regular_ngon(n=5,r=5))], z=[0,4], refine=10, slices=10);
 264// Example(FlatSpin,VPD=185): In this example reindexing does not fix the orientation of the triangle because it happens in 3d within skin(), so we have to reverse the triangle manually
 265//   ellipse = yscale(3,circle(r=10, $fn=32));
 266//   tri = move([-50/3,-9],[[0,0], [50,0], [0,27]]);
 267//   skin([ellipse, reverse(tri)], z=[0,20], slices=20, method="reindex");
 268// Example(FlatSpin,VPD=185): You can get a nicer transition by rotating the polygons for better alignment.  You have to resample yourself before calling `align_polygon`. The orientation is fixed so we do not need to reverse.
 269//   ellipse = yscale(3,circle(r=10, $fn=32));
 270//   tri = move([-50/3,-9],
 271//              subdivide_path([[0,0], [50,0], [0,27]], 32));
 272//   aligned = align_polygon(ellipse,tri, [0:5:180]);
 273//   skin([ellipse, aligned], z=[0,20], slices=20);
 274// Example(FlatSpin,VPD=35): The "distance" method is a completely different approach.
 275//   skin([regular_ngon(n=4, r=4), regular_ngon(n=5,r=5)], z=[0,4], refine=10, slices=10, method="distance");
 276// Example(FlatSpin,VPD=35,VPT=[0,0,4]): Connecting pentagon to heptagon inserts two triangular faces on each side
 277//   small = path3d(circle(r=3, $fn=5));
 278//   big = up(2,p=yrot( 0,p=path3d(circle(r=3, $fn=7), 6)));
 279//   skin([small,big],method="distance", slices=10, refine=10);
 280// Example(FlatSpin,VPD=35,VPT=[0,0,4]): But just a slight rotation of the top profile moves the two triangles to one end
 281//   small = path3d(circle(r=3, $fn=5));
 282//   big = up(2,p=yrot(14,p=path3d(circle(r=3, $fn=7), 6)));
 283//   skin([small,big],method="distance", slices=10, refine=10);
 284// Example(FlatSpin,VPD=32,VPT=[1.2,4.3,2]): Another "distance" example:
 285//   off = [0,2];
 286//   shape = turtle(["right",45,"move", "left",45,"move", "left",45, "move", "jump", [.5+sqrt(2)/2,8]]);
 287//   rshape = rot(180,cp=centroid(shape)+off, p=shape);
 288//   skin([shape,rshape],z=[0,4], method="distance",slices=10,refine=15);
 289// Example(FlatSpin,VPD=32,VPT=[1.2,4.3,2]): Slightly shifting the profile changes the optimal linkage
 290//   off = [0,1];
 291//   shape = turtle(["right",45,"move", "left",45,"move", "left",45, "move", "jump", [.5+sqrt(2)/2,8]]);
 292//   rshape = rot(180,cp=centroid(shape)+off, p=shape);
 293//   skin([shape,rshape],z=[0,4], method="distance",slices=10,refine=15);
 294// Example(FlatSpin,VPD=444,VPT=[0,0,50]): This optimal solution doesn't look terrible:
 295//   prof1 = path3d([[-50,-50], [-50,50], [50,50], [25,25], [50,0], [25,-25], [50,-50]]);
 296//   prof2 = path3d(regular_ngon(n=7, r=50),100);
 297//   skin([prof1, prof2], method="distance", slices=10, refine=10);
 298// Example(FlatSpin,VPD=444,VPT=[0,0,50]): But this one looks better.  The "distance" method doesn't find it because it uses two more edges, so it clearly has a higher total edge distance.  We force it by doubling the first two vertices of one of the profiles.
 299//   prof1 = path3d([[-50,-50], [-50,50], [50,50], [25,25], [50,0], [25,-25], [50,-50]]);
 300//   prof2 = path3d(regular_ngon(n=7, r=50),100);
 301//   skin([repeat_entries(prof1,[2,2,1,1,1,1,1]),
 302//         prof2],
 303//        method="distance", slices=10, refine=10);
 304// Example(FlatSpin,VPD=80,VPT=[0,0,7]): The "distance" method will often produces results similar to the "tangent" method if you use it with a polygon and a curve, but the results can also look like this:
 305//   skin([path3d(circle($fn=128, r=10)), xrot(39, p=path3d(square([8,10]),10))],  method="distance", slices=0);
 306// Example(FlatSpin,VPD=80,VPT=[0,0,7]): Using the "tangent" method produces:
 307//   skin([path3d(circle($fn=128, r=10)), xrot(39, p=path3d(square([8,10]),10))],  method="tangent", slices=0);
 308// Example(FlatSpin,VPD=74): Torus using hexagons and pentagons, where `closed=true`
 309//   hex = right(7,p=path3d(hexagon(r=3)));
 310//   pent = right(7,p=path3d(pentagon(r=3)));
 311//   N=5;
 312//   skin(
 313//        [for(i=[0:2*N-1]) yrot(360*i/2/N, p=(i%2==0 ? hex : pent))],
 314//        refine=1,slices=0,method="distance",closed=true);
 315// Example: A smooth morph is achieved when you can calculate all the slices yourself.  Since you provide all the slices, set `slices=0`.
 316//   skin([for(n=[.1:.02:.5])
 317//            yrot(n*60-.5*60,p=path3d(supershape(step=360/128,m1=5,n1=n, n2=1.7),5-10*n))],
 318//        slices=0);
 319// Example: Another smooth supershape morph:
 320//   skin([for(alpha=[-.2:.05:1.5])
 321//            path3d(supershape(step=360/256,m1=7, n1=lerp(2,3,alpha),
 322//                              n2=lerp(8,4,alpha), n3=lerp(4,17,alpha)),alpha*5)],
 323//        slices=0);
 324// Example: Several polygons connected using "distance"
 325//   skin([regular_ngon(n=4, r=3),
 326//         regular_ngon(n=6, r=3),
 327//         regular_ngon(n=9, r=4),
 328//         rot(17,p=regular_ngon(n=6, r=3)),
 329//         rot(37,p=regular_ngon(n=4, r=3))],
 330//        z=[0,2,4,6,9], method="distance", slices=10, refine=10);
 331// Example(FlatSpin,VPD=935,VPT=[75,0,123]): Vertex count of the polygon changes at every profile
 332//   skin([
 333//       for (ang = [0:10:90])
 334//       rot([0,ang,0], cp=[200,0,0], p=path3d(circle(d=100,$fn=12-(ang/10))))
 335//   ],method="distance",slices=10,refine=10);
 336// Example: Möbius Strip.  This is a tricky model because when you work your way around to the connection, the direction of the profiles is flipped, so how can the proper geometry be created?  The trick is to duplicate the first profile and turn the caps off.  The model closes up and forms a valid polyhedron.
 337//   skin([
 338//     for (ang = [0:5:360])
 339//     rot([0,ang,0], cp=[100,0,0], p=rot(ang/2, p=path3d(square([1,30],center=true))))
 340//   ], caps=false, slices=0, refine=20);
 341// Example: This model of two scutoids packed together is based on https://www.thingiverse.com/thing:3024272 by mathgrrl
 342//   sidelen = 10;  // Side length of scutoid
 343//   height = 25;   // Height of scutoid
 344//   angle = -15;   // Angle (twists the entire form)
 345//   push = -5;     // Push (translates the base away from the top)
 346//   flare = 1;     // Flare (the two pieces will be different unless this is 1)
 347//   midpoint = .5; // Height of the extra vertex (as a fraction of total height); the two pieces will be different unless this is .5)
 348//   pushvec = rot(angle/2,p=push*RIGHT);  // Push direction is the the average of the top and bottom mating edges
 349//   pent = path3d(apply(move(pushvec)*rot(angle),pentagon(side=sidelen,align_side=RIGHT,anchor="side0")));
 350//   hex = path3d(hexagon(side=flare*sidelen, align_side=RIGHT, anchor="side0"),height);
 351//   pentmate = path3d(pentagon(side=flare*sidelen,align_side=LEFT,anchor="side0"),height);
 352//             // Native index would require mapping first and last vertices together, which is not allowed, so shift
 353//   hexmate = list_rotate(
 354//                           path3d(apply(move(pushvec)*rot(angle),hexagon(side=sidelen,align_side=LEFT,anchor="side0"))),
 355//                           -1);
 356//   join_vertex = lerp(
 357//                       mean(select(hex,1,2)),     // midpoint of "extra" hex edge
 358//                       mean(select(hexmate,0,1)), // midpoint of "extra" hexmate edge
 359//                       midpoint);
 360//   augpent = repeat_entries(pent, [1,2,1,1,1]);         // Vertex 1 will split at the top forming a triangular face with the hexagon
 361//   augpent_mate = repeat_entries(pentmate,[2,1,1,1,1]); // For mating pentagon it is vertex 0 that splits
 362//              // Middle is the interpolation between top and bottom except for the join vertex, which is doubled because it splits
 363//   middle = list_set(lerp(augpent,hex,midpoint),[1,2],[join_vertex,join_vertex]);
 364//   middle_mate = list_set(lerp(hexmate,augpent_mate,midpoint), [0,1], [join_vertex,join_vertex]);
 365//   skin([augpent,middle,hex],  slices=10, refine=10, sampling="segment");
 366//   color("green")skin([augpent_mate,middle_mate,hexmate],  slices=10,refine=10, sampling="segment");
 367// Example: If you create a self-intersecting polyhedron the result is invalid.  In some cases self-intersection may be obvous.  Here is a more subtle example.
 368//   skin([
 369//          for (a = [0:30:180]) let(
 370//              pos  = [-60*sin(a),     0, a    ],
 371//              pos2 = [-60*sin(a+0.1), 0, a+0.1]
 372//          ) move(pos,
 373//              p=rot(from=UP, to=pos2-pos,
 374//                  p=path3d(circle(d=150))
 375//              )
 376//          )
 377//      ],refine=1,slices=0);
 378//      color("red") {
 379//          zrot(25) fwd(130) xrot(75) {
 380//              linear_extrude(height=0.1) {
 381//                  ydistribute(25) {
 382//                      text(text="BAD POLYHEDRONS!", size=20, halign="center", valign="center");
 383//                      text(text="CREASES MAKE", size=20, halign="center", valign="center");
 384//                  }
 385//              }
 386//          }
 387//          up(160) zrot(25) fwd(130) xrot(75) {
 388//              stroke(zrot(30, p=yscale(0.5, p=circle(d=120))),width=10,closed=true);
 389//          }
 390//      }
 391module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, style="min_edge", convexity=10,
 392            anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull")
 393{
 394    vnf = skin(profiles, slices, refine, method, sampling, caps, closed, z, style=style);
 395    vnf_polyhedron(vnf,convexity=convexity,spin=spin,anchor=anchor,orient=orient,atype=atype,cp=cp)
 396        children();
 397}
 398
 399
 400function skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, style="min_edge",
 401              anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull") =
 402  assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"")
 403  assert(is_def(slices),"The slices argument must be specified.")
 404  assert(is_list(profiles) && len(profiles)>1, "Must provide at least two profiles")
 405  let(
 406       profiles = [for(p=profiles) if (is_region(p) && len(p)==1) p[0] else p]
 407  )
 408  let( bad = [for(i=idx(profiles)) if (!(is_path(profiles[i]) && len(profiles[i])>2)) i])
 409  assert(len(bad)==0, str("Profiles ",bad," are not a paths or have length less than 3"))
 410  let(
 411    profcount = len(profiles) - (closed?0:1),
 412    legal_methods = ["direct","reindex","distance","fast_distance","tangent"],
 413    caps = is_def(caps) ? caps :
 414           closed ? false : true,
 415    capsOK = is_bool(caps) || is_bool_list(caps,2),
 416    fullcaps = is_bool(caps) ? [caps,caps] : caps,
 417    refine = is_list(refine) ? refine : repeat(refine, len(profiles)),
 418    slices = is_list(slices) ? slices : repeat(slices, profcount),
 419    refineOK = [for(i=idx(refine)) if (refine[i]<=0 || !is_integer(refine[i])) i],
 420    slicesOK = [for(i=idx(slices)) if (!is_integer(slices[i]) || slices[i]<0) i],
 421    maxsize = max_length(profiles),
 422    methodok = is_list(method) || in_list(method, legal_methods),
 423    methodlistok = is_list(method) ? [for(i=idx(method)) if (!in_list(method[i], legal_methods)) i] : [],
 424    method = is_string(method) ? repeat(method, profcount) : method,
 425    // Define to be zero where a resampling method is used and 1 where a vertex duplicator is used
 426    RESAMPLING = 0,
 427    DUPLICATOR = 1,
 428    method_type = [for(m = method) m=="direct" || m=="reindex" ? 0 : 1],
 429    sampling = is_def(sampling) ? sampling :
 430               in_list(DUPLICATOR,method_type) ? "segment" : "length"
 431  )
 432  assert(len(refine)==len(profiles), "refine list is the wrong length")
 433  assert(len(slices)==profcount, str("slices list must have length ",profcount))
 434  assert(slicesOK==[],str("slices must be nonnegative integers"))
 435  assert(refineOK==[],str("refine must be postive integer"))
 436  assert(methodok,str("method must be one of ",legal_methods,". Got ",method))
 437  assert(methodlistok==[], str("method list contains invalid method at ",methodlistok))
 438  assert(len(method) == profcount,"Method list is the wrong length")
 439  assert(in_list(sampling,["length","segment"]), "sampling must be set to \"length\" or \"segment\"")
 440  assert(sampling=="segment" || (!in_list("distance",method) && !in_list("fast_distance",method) && !in_list("tangent",method)), "sampling is set to \"length\" which is only allowed with methods \"direct\" and \"reindex\"")
 441  assert(capsOK, "caps must be boolean or a list of two booleans")
 442  assert(!closed || !caps, "Cannot make closed shape with caps")
 443  let(
 444    profile_dim=list_shape(profiles,2),
 445    profiles_zcheck = (profile_dim != 2) || (profile_dim==2 && is_list(z) && len(z)==len(profiles)),
 446    profiles_ok = (profile_dim==2 && is_list(z) && len(z)==len(profiles)) || profile_dim==3
 447  )
 448  assert(profiles_zcheck, "z parameter is invalid or has the wrong length.")
 449  assert(profiles_ok,"Profiles must all be 3d or must all be 2d, with matching length z parameter.")
 450  assert(is_undef(z) || profile_dim==2, "Do not specify z with 3d profiles")
 451  assert(profile_dim==3 || len(z)==len(profiles),"Length of z does not match length of profiles.")
 452  let(
 453    // Adjoin Z coordinates to 2d profiles
 454    profiles = profile_dim==3 ? profiles :
 455               [for(i=idx(profiles)) path3d(profiles[i], z[i])],
 456    // True length (not counting repeated vertices) of profiles after refinement
 457    refined_len = [for(i=idx(profiles)) refine[i]*len(profiles[i])],
 458    // Define this to be 1 if a profile is used on either side by a resampling method, zero otherwise.
 459    profile_resampled = [for(i=idx(profiles))
 460      1-(
 461           i==0 ?  method_type[0] * (closed? last(method_type) : 1) :
 462           i==len(profiles)-1 ? last(method_type) * (closed ? select(method_type,-2) : 1) :
 463         method_type[i] * method_type[i-1])],
 464    parts = search(1,[1,for(i=[0:1:len(profile_resampled)-2]) profile_resampled[i]!=profile_resampled[i+1] ? 1 : 0],0),
 465    plen = [for(i=idx(parts)) (i== len(parts)-1? len(refined_len) : parts[i+1]) - parts[i]],
 466    max_list = [for(i=idx(parts)) each repeat(max(select(refined_len, parts[i], parts[i]+plen[i]-1)), plen[i])],
 467    transition_profiles = [for(i=[(closed?0:1):1:profcount-1]) if (select(method_type,i-1) != method_type[i]) i],
 468    badind = [for(tranprof=transition_profiles) if (refined_len[tranprof] != max_list[tranprof]) tranprof]
 469  )
 470  assert(badind==[],str("Profile length mismatch at method transition at indices ",badind," in skin()"))
 471  let(
 472    full_list =    // If there are no duplicators then use more efficient where the whole input is treated together
 473      !in_list(DUPLICATOR,method_type) ?
 474         let(
 475             resampled = [for(i=idx(profiles)) subdivide_path(profiles[i], max_list[i], method=sampling)],
 476             fixedprof = [for(i=idx(profiles))
 477                             i==0 || method[i-1]=="direct" ? resampled[i]
 478                                                         : reindex_polygon(resampled[i-1],resampled[i])],
 479             sliced = slice_profiles(fixedprof, slices, closed)
 480            )
 481            [!closed ? sliced : concat(sliced,[sliced[0]])]
 482      :  // There are duplicators, so use approach where each pair is treated separately
 483      [for(i=[0:profcount-1])
 484        let(
 485          pair =
 486            method[i]=="distance" ? _skin_distance_match(profiles[i],select(profiles,i+1)) :
 487            method[i]=="fast_distance" ? _skin_aligned_distance_match(profiles[i], select(profiles,i+1)) :
 488            method[i]=="tangent" ? _skin_tangent_match(profiles[i],select(profiles,i+1)) :
 489            /*method[i]=="reindex" || method[i]=="direct" ?*/
 490               let( p1 = subdivide_path(profiles[i],max_list[i], method=sampling),
 491                    p2 = subdivide_path(select(profiles,i+1),max_list[i], method=sampling)
 492               ) (method[i]=="direct" ? [p1,p2] : [p1, reindex_polygon(p1, p2)]),
 493            nsamples =  method_type[i]==RESAMPLING ? len(pair[0]) :
 494               assert(refine[i]==select(refine,i+1),str("Refine value mismatch at indices ",[i,(i+1)%len(refine)],
 495                                                        ".  Method ",method[i]," requires equal values"))
 496               refine[i] * len(pair[0])
 497          )
 498          subdivide_and_slice(pair,slices[i], nsamples, method=sampling)],
 499      vnf=vnf_join(
 500          [for(i=idx(full_list))
 501              vnf_vertex_array(full_list[i], cap1=i==0 && fullcaps[0], cap2=i==len(full_list)-1 && fullcaps[1],
 502                               col_wrap=true, style=style)])
 503  )
 504  reorient(anchor,spin,orient,vnf=vnf,p=vnf,extent=atype=="hull",cp=cp);
 505
 506
 507
 508// Function&Module: linear_sweep()
 509// Usage:
 510//   linear_sweep(region, [height], [center=], [slices=], [twist=], [scale=], [style=], [convexity=]) [ATTACHMENTS];
 511// Usage: With Texturing
 512//   linear_sweep(region, [height], [center=], texture=, [tex_size=]|[tex_counts=], [tex_scale=], [style=], [tex_samples=], ...) [ATTACHMENTS];
 513// Description:
 514//   If called as a module, creates a polyhedron that is the linear extrusion of the given 2D region or polygon.
 515//   If called as a function, returns a VNF that can be used to generate a polyhedron of the linear extrusion
 516//   of the given 2D region or polygon.  The benefit of using this, over using `linear_extrude region(rgn)` is
 517//   that it supports `anchor`, `spin`, `orient` and attachments.  You can also make more refined
 518//   twisted extrusions by using `maxseg` to subsample flat faces.
 519// Arguments:
 520//   region = The 2D [Region](regions.scad) or polygon that is to be extruded.
 521//   h / height = The height to extrude the region.  Default: 1
 522//   center = If true, the created polyhedron will be vertically centered.  If false, it will be extruded upwards from the XY plane.  Default: `false`
 523//   ---
 524//   twist = The number of degrees to rotate the top of the shape, clockwise around the Z axis, relative to the bottom.  Default: 0
 525//   scale = The amount to scale the top of the shape, in the X and Y directions, relative to the size of the bottom.  Default: 1
 526//   shift = The amount to shift the top of the shape, in the X and Y directions, relative to the position of the bottom.  Default: [0,0]
 527//   slices = The number of slices to divide the shape into along the Z axis, to allow refinement of detail, especially when working with a twist.  Default: `twist/5`
 528//   maxseg = If given, then any long segments of the region will be subdivided to be shorter than this length.  This can refine twisting flat faces a lot.  Default: `undef` (no subsampling)
 529//   texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to vertical surfaces.  See {{texture()}} for what named textures are supported.
 530//   tex_size = An optional 2D target size for the textures.  Actual texture sizes will be scaled somewhat to evenly fit the available surface. Default: `[5,5]`
 531//   tex_counts = If given instead of tex_size, gives the tile repetition counts for textures over the surface length and height.
 532//   tex_inset = If numeric, lowers the texture into the surface by that amount, before the tex_scale multiplier is applied.  If `true`, insets by exactly `1`.  Default: `false`
 533//   tex_rot = If true, rotates the texture 90º.
 534//   tex_scale = Scaling multiplier for the texture depth.
 535//   tex_samples = Minimum number of "bend points" to have in VNF texture tiles.  Default: 8
 536//   style = The style to use when triangulating the surface of the object.  Valid values are `"default"`, `"alt"`, or `"quincunx"`.
 537//   convexity = Max number of surfaces any single ray could pass through.  Module use only.
 538//   cp = Centerpoint for determining intersection anchors or centering the shape.  Determines the base of the anchor vector.  Can be "centroid", "mean", "box" or a 3D point.  Default: `"centroid"`
 539//   atype = Set to "hull" or "intersect" to select anchor type.  Default: "hull"
 540//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `"origin"`
 541//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
 542//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
 543// Anchor Types:
 544//   "hull" = Anchors to the virtual convex hull of the shape.
 545//   "intersect" = Anchors to the surface of the shape.
 546//   "bbox" = Anchors to the bounding box of the extruded shape.
 547// Extra Anchors:
 548//   "origin" = Centers the extruded shape vertically only, but keeps the original path positions in the X and Y.  Oriented UP.
 549//   "original_base" = Keeps the original path positions in the X and Y, but at the bottom of the extrusion.  Oriented UP.
 550// Example: Extruding a Compound Region.
 551//   rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
 552//   rgn2 = [square(30,center=false)];
 553//   rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
 554//   mrgn = union(rgn1,rgn2);
 555//   orgn = difference(mrgn,rgn3);
 556//   linear_sweep(orgn,height=20,convexity=16);
 557// Example: With Twist, Scale, Shift, Slices and Maxseg.
 558//   rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
 559//   rgn2 = [square(30,center=false)];
 560//   rgn3 = [
 561//       for (size=[10:10:20])
 562//       apply(
 563//          move([15,15]),
 564//          square(size=size, center=true)
 565//       )
 566//   ];
 567//   mrgn = union(rgn1,rgn2);
 568//   orgn = difference(mrgn,rgn3);
 569//   linear_sweep(
 570//       orgn, height=50, maxseg=2, slices=40,
 571//       twist=90, scale=0.5, shift=[10,5],
 572//       convexity=16
 573//   );
 574// Example: Anchors on an Extruded Region
 575//   rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
 576//   rgn2 = [square(30,center=false)];
 577//   rgn3 = [
 578//       for (size=[10:10:20])
 579//       apply(
 580//           move([15,15]),
 581//           rect(size=size)
 582//       )
 583//   ];
 584//   mrgn = union(rgn1,rgn2);
 585//   orgn = difference(mrgn,rgn3);
 586//   linear_sweep(orgn,height=20,convexity=16)
 587//       show_anchors();
 588// Example: "diamonds" texture.
 589//   path = glued_circles(r=15, spread=40, tangent=45);
 590//   linear_sweep(
 591//       path, texture="diamonds", tex_size=[5,10],
 592//       h=40, style="concave");
 593// Example: "pyramids" texture.
 594//   linear_sweep(
 595//       rect(50), texture="pyramids", tex_size=[10,10],
 596//       h=40, style="convex");
 597// Example: "bricks_vnf" texture.
 598//   path = glued_circles(r=15, spread=40, tangent=45);
 599//   linear_sweep(
 600//       path, texture="bricks_vnf", tex_size=[10,10],
 601//       tex_scale=0.25, h=40);
 602// Example: User defined heightfield texture.
 603//   path = ellipse(r=[20,10]);
 604//   texture = [for (i=[0:9])
 605//       [for (j=[0:9])
 606//           1/max(0.5,norm([i,j]-[5,5])) ]];
 607//   linear_sweep(
 608//       path, texture=texture, tex_size=[5,5],
 609//       h=40, style="min_edge", anchor=BOT);
 610// Example: User defined VNF tile texture.
 611//   path = ellipse(r=[20,10]);
 612//   tex = let(n=16,m=0.25) [
 613//        [
 614//            each resample_path(path3d(square(1)),n),
 615//            each move([0.5,0.5],
 616//                p=path3d(circle(d=0.5,$fn=n),m)),
 617//            [1/2,1/2,0],
 618//        ], [
 619//            for (i=[0:1:n-1]) each [
 620//                [i,(i+1)%n,(i+3)%n+n],
 621//                [i,(i+3)%n+n,(i+2)%n+n],
 622//                [2*n,n+i,n+(i+1)%n],
 623//            ]
 624//        ]
 625//   ];
 626//   linear_sweep(path, texture=tex, tex_size=[5,5], h=40);
 627// Example: As Function
 628//   path = glued_circles(r=15, spread=40, tangent=45);
 629//   vnf = linear_sweep(
 630//       path, h=40, texture="trunc_pyramids", tex_size=[5,5],
 631//       tex_scale=1, style="convex");
 632//   vnf_polyhedron(vnf, convexity=10);
 633
 634module linear_sweep(
 635    region, height, center,
 636    twist=0, scale=1, shift=[0,0],
 637    slices, maxseg, style="default", convexity,
 638    texture, tex_size=[5,5], tex_counts,
 639    tex_inset=false, tex_rot=false,
 640    tex_scale=1, tex_samples,
 641    cp, atype="hull", h,
 642    anchor, spin=0, orient=UP
 643) {
 644    h = first_defined([h, height, 1]);
 645    region = force_region(region);
 646    check = assert(is_region(region),"Input is not a region");
 647    anchor = center==true? "origin" :
 648        center == false? "original_base" :
 649        default(anchor, "original_base");
 650    vnf = linear_sweep(
 651        region, height=h, style=style,
 652        twist=twist, scale=scale, shift=shift,
 653        texture=texture,
 654        tex_size=tex_size,
 655        tex_counts=tex_counts,
 656        tex_inset=tex_inset,
 657        tex_rot=tex_rot,
 658        tex_scale=tex_scale,
 659        tex_samples=tex_samples,
 660        slices=slices,
 661        maxseg=maxseg,
 662        anchor="origin"
 663    );
 664    anchors = [
 665        named_anchor("original_base", [0,0,-h/2], UP)
 666    ];
 667    cp = default(cp, "centroid");
 668    geom = atype=="hull"?  attach_geom(cp=cp, region=region, h=h, extent=true, shift=shift, scale=scale, twist=twist, anchors=anchors) :
 669        atype=="intersect"?  attach_geom(cp=cp, region=region, h=h, extent=false, shift=shift, scale=scale, twist=twist, anchors=anchors) :
 670        atype=="bbox"?
 671            let(
 672                bounds = pointlist_bounds(flatten(region)),
 673                size = bounds[1] - bounds[0],
 674                midpt = (bounds[0] + bounds[1])/2
 675            )
 676            attach_geom(cp=[0,0,0], size=point3d(size,h), offset=point3d(midpt), shift=shift, scale=scale, twist=twist, anchors=anchors) :
 677        assert(in_list(atype, ["hull","intersect","bbox"]), "Anchor type must be \"hull\", \"intersect\", or \"bbox\".");
 678    attachable(anchor,spin,orient, geom=geom) {
 679        vnf_polyhedron(vnf, convexity=convexity);
 680        children();
 681    }
 682}
 683
 684
 685function linear_sweep(
 686    region, height, center,
 687    twist=0, scale=1, shift=[0,0],
 688    slices, maxseg, style="default",
 689    cp, atype="hull", h,
 690    texture, tex_size=[5,5], tex_counts,
 691    tex_inset=false, tex_rot=false,
 692    tex_scale=1, tex_samples,
 693    anchor, spin=0, orient=UP
 694) =
 695    let( region = force_region(region) )
 696    assert(is_region(region), "Input is not a region or polygon.")
 697    assert(is_num(scale) || is_vector(scale))
 698    assert(is_vector(shift, 2), str(shift))
 699    let(
 700        h = first_defined([h, height, 1])
 701    )
 702    !is_undef(texture)? _textured_linear_sweep(
 703        region, h=h,
 704        texture=texture, tex_size=tex_size,
 705        counts=tex_counts, inset=tex_inset,
 706        rot=tex_rot, tex_scale=tex_scale,
 707        twist=twist, scale=scale, shift=shift,
 708        style=style, samples=tex_samples,
 709        anchor=anchor, spin=spin, orient=orient
 710    ) :
 711    let(
 712        anchor = center==true? "origin" :
 713            center == false? "original_base" :
 714            default(anchor, "original_base"),
 715        regions = region_parts(region),
 716        slices = default(slices, max(1,ceil(abs(twist)/5))),
 717        scale = is_num(scale)? [scale,scale] : point2d(scale),
 718        topmat = move(shift) * scale(scale) * rot(-twist),
 719        trgns = [
 720            for (rgn = regions) [
 721                for (path = rgn) let(
 722                    p = cleanup_path(path),
 723                    path = is_undef(maxseg)? p : [
 724                        for (seg = pair(p,true)) each
 725                        let( steps = ceil(norm(seg.y - seg.x) / maxseg) )
 726                        lerpn(seg.x, seg.y, steps, false)
 727                    ]
 728                ) apply(topmat, path)
 729            ]
 730        ],
 731        vnf = vnf_join([
 732            for (rgn = regions)
 733            for (pathnum = idx(rgn)) let(
 734                p = cleanup_path(rgn[pathnum]),
 735                path = is_undef(maxseg)? p : [
 736                    for (seg=pair(p,true)) each
 737                    let(steps=ceil(norm(seg.y-seg.x)/maxseg))
 738                    lerpn(seg.x, seg.y, steps, false)
 739                ],
 740                verts = [
 741                    for (i=[0:1:slices]) let(
 742                        u = i / slices,
 743                        scl = lerp([1,1], scale, u),
 744                        ang = lerp(0, -twist, u),
 745                        off = lerp([0,0,-h/2], point3d(shift,h/2), u),
 746                        m = move(off) * scale(scl) * rot(ang)
 747                    ) apply(m, path3d(path))
 748                ]
 749            ) vnf_vertex_array(verts, caps=false, col_wrap=true, style=style),
 750            for (rgn = regions) vnf_from_region(rgn, down(h/2), reverse=true),
 751            for (rgn = trgns) vnf_from_region(rgn, up(h/2), reverse=false)
 752        ]),
 753        anchors = [
 754            named_anchor("original_base", [0,0,-h/2], UP)
 755        ],
 756        cp = default(cp, "centroid"),
 757        geom = atype=="hull"?  attach_geom(cp=cp, region=region, h=h, extent=true, shift=shift, scale=scale, twist=twist, anchors=anchors) :
 758            atype=="intersect"?  attach_geom(cp=cp, region=region, h=h, extent=false, shift=shift, scale=scale, twist=twist, anchors=anchors) :
 759            atype=="bbox"?
 760                let(
 761                    bounds = pointlist_bounds(flatten(region)),
 762                    size = bounds[1] - bounds[0],
 763                    midpt = (bounds[0] + bounds[1])/2
 764                )
 765                attach_geom(cp=[0,0,0], size=point3d(size,h), offset=point3d(midpt), shift=shift, scale=scale, twist=twist, anchors=anchors) :
 766            assert(in_list(atype, ["hull","intersect","bbox"]), "Anchor type must be \"hull\", \"intersect\", or \"bbox\".")
 767    ) reorient(anchor,spin,orient, geom=geom, p=vnf);
 768
 769
 770// Function&Module: rotate_sweep()
 771// Usage: As Function
 772//   vnf = rotate_sweep(shape, [angle], ...);
 773// Usage: As Module
 774//   rotate_sweep(shape, [angle], ...) [ATTACHMENTS];
 775// Usage: With Texturing
 776//   rotate_sweep(shape, texture=, [tex_size=]|[tex_counts=], [tex_scale=], [tex_samples=], [tex_rot=], [tex_inset=], ...) [ATTACHMENTS];
 777// Topics: Extrusion, Sweep, Revolution
 778// Description:
 779//   Takes a polygon or [region](regions.scad) and sweeps it in a rotation around the Z axis, with optional texturing.
 780//   When called as a function, returns a [VNF](vnf.scad).
 781//   When called as a module, creates the sweep as geometry.
 782// Arguments:
 783//   shape = The polygon or [region](regions.scad) to sweep around the Z axis.
 784//   angle = If given, specifies the number of degrees to sweep the shape around the Z axis, counterclockwise from the X+ axis.  Default: 360 (full rotation)
 785//   ---
 786//   texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to vertical surfaces.  See {{texture()}} for what named textures are supported.
 787//   tex_size = An optional 2D target size for the textures.  Actual texture sizes will be scaled somewhat to evenly fit the available surface. Default: `[5,5]`
 788//   tex_counts = If given instead of tex_size, gives the tile repetition counts for textures over the surface length and height.
 789//   tex_inset = If numeric, lowers the texture into the surface by that amount, before the tex_scale multiplier is applied.  If `true`, insets by exactly `1`.  Default: `false`
 790//   tex_rot = If true, rotates the texture 90º.
 791//   tex_scale = Scaling multiplier for the texture depth.
 792//   tex_samples = Minimum number of "bend points" to have in VNF texture tiles.  Default: 8
 793//   style = {{vnf_vertex_array()}} style.  Default: "min_edge"
 794//   closed = If false, and shape is given as a path, then the revolved path will be sealed to the axis of rotation with untextured caps.  Default: `true`
 795//   convexity = (Module only) Convexity setting for use with polyhedron.  Default: 10
 796//   cp = Centerpoint for determining "intersect" anchors or centering the shape.  Determintes the base of the anchor vector.  Can be "centroid", "mean", "box" or a 3D point.  Default: "centroid"
 797//   atype = Select "hull" or "intersect" anchor types.  Default: "hull"
 798//   anchor = Translate so anchor point is at the origin. Default: "origin"
 799//   spin = Rotate this many degrees around Z axis after anchor. Default: 0
 800//   orient = Vector to rotate top towards after spin  (module only)
 801// Anchor Types:
 802//   "hull" = Anchors to the virtual convex hull of the shape.
 803//   "intersect" = Anchors to the surface of the shape.
 804// See Also: linear_sweep(), sweep()
 805// Example:
 806//   rgn = [
 807//       for (a = [0, 120, 240]) let(
 808//           cp = polar_to_xy(15, a) + [30,0]
 809//       ) each [
 810//           move(cp, p=circle(r=10)),
 811//           move(cp, p=hexagon(d=15)),
 812//       ]
 813//   ];
 814//   rotate_sweep(rgn, angle=240);
 815// Example:
 816//   rgn = right(30, p=union([for (a = [0, 90]) rot(a, p=rect([15,5]))]));
 817//   rotate_sweep(rgn);
 818// Example:
 819//   path = right(50, p=circle(d=40));
 820//   rotate_sweep(path, texture="bricks_vnf", tex_size=[10,10], tex_scale=0.5, style="concave");
 821// Example:
 822//   tex = [
 823//       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 824//       [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
 825//       [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
 826//       [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1],
 827//       [0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1],
 828//       [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1],
 829//       [0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1],
 830//       [0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1],
 831//       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
 832//       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
 833//       [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
 834//       [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
 835//   ];
 836//   path = arc(cp=[0,0], r=40, start=60, angle=-120);
 837//   rotate_sweep(
 838//       path, closed=false,
 839//       texture=tex, tex_size=[20,20],
 840//       tex_scale=1, style="concave");
 841// Example:
 842//   include <BOSL2/beziers.scad>
 843//   bezpath = [
 844//       [15, 30], [10,15],
 845//       [10,  0], [20, 10], [30,12],
 846//       [30,-12], [20,-10], [10, 0],
 847//       [10,-15], [15,-30]
 848//   ];
 849//   path = bezpath_curve(bezpath, splinesteps=32);
 850//   rotate_sweep(
 851//       path, closed=false,
 852//       texture="diamonds", tex_size=[10,10],
 853//       tex_scale=1, style="concave");
 854// Example:
 855//   path = [
 856//       [20, 30], [20, 20],
 857//       each arc(r=20, corner=[[20,20],[10,0],[20,-20]]),
 858//       [20,-20], [20,-30],
 859//   ];
 860//   vnf = rotate_sweep(
 861//       path, closed=false,
 862//       texture="trunc_pyramids",
 863//       tex_size=[5,5], tex_scale=1,
 864//       style="convex");
 865//   vnf_polyhedron(vnf, convexity=10);
 866// Example:
 867//   rgn = [
 868//       right(40, p=circle(d=50)),
 869//       right(40, p=circle(d=40,$fn=6)),
 870//   ];
 871//   rotate_sweep(
 872//       rgn, texture="diamonds",
 873//       tex_size=[10,10], tex_scale=1,
 874//       angle=240, style="concave");
 875
 876function rotate_sweep(
 877    shape, angle=360,
 878    texture, tex_size=[5,5], tex_counts,
 879    tex_inset=false, tex_rot=false,
 880    tex_scale=1, tex_samples,
 881    tex_taper, shift=[0,0], closed=true,
 882    style="min_edge", cp="centroid",
 883    atype="hull", anchor="origin",
 884    spin=0, orient=UP
 885) =
 886    let( region = force_region(shape) )
 887    assert(is_region(region), "Input is not a region or polygon.")
 888    let(
 889        bounds = pointlist_bounds(flatten(region)),
 890        min_x = bounds[0].x,
 891        max_x = bounds[1].x,
 892        min_y = bounds[0].y,
 893        max_y = bounds[1].y,
 894        h = max_y - min_y
 895    )
 896    assert(min_x>=0, "Input region must exist entirely in the X+ half-plane.")
 897    !is_undef(texture)? _textured_revolution(
 898        shape,
 899        texture=texture,
 900        tex_size=tex_size,
 901        counts=tex_counts,
 902        tex_scale=tex_scale,
 903        inset=tex_inset,
 904        rot=tex_rot,
 905        samples=tex_samples,
 906        taper=tex_taper,
 907        shift=shift,
 908        closed=closed,
 909        angle=angle,
 910        style=style
 911    ) :
 912    let(
 913        steps = segs(max_x),
 914        skmat = down(min_y) * skew(sxz=shift.x/h, syz=shift.y/h) * up(min_y),
 915        transforms = [
 916            if (angle==360) for (i=[0:1:steps-1]) skmat * rot([90,0,360-i*360/steps]),
 917            if (angle<360) for (i=[0:1:steps-1]) skmat * rot([90,0,angle-i*angle/(steps-1)]),
 918        ],
 919        vnf = sweep(
 920            region, transforms,
 921            closed=angle==360,
 922            caps=angle!=360,
 923            style=style, cp=cp,
 924            atype=atype, anchor=anchor,
 925            spin=spin, orient=orient
 926        )
 927    ) vnf;
 928
 929
 930module rotate_sweep(
 931    shape, angle=360,
 932    texture, tex_size=[5,5], tex_counts,
 933    tex_inset=false, tex_rot=false,
 934    tex_scale=1, tex_samples,
 935    tex_taper, shift=[0,0],
 936    style="min_edge",
 937    closed=true,
 938    cp="centroid",
 939    convexity=10,
 940    atype="hull",
 941    anchor="origin",
 942    spin=0,
 943    orient=UP
 944) {
 945    region = force_region(shape);
 946    check = assert(is_region(region), "Input is not a region or polygon.");
 947    bounds = pointlist_bounds(flatten(region));
 948    min_x = bounds[0].x;
 949    max_x = bounds[1].x;
 950    min_y = bounds[0].y;
 951    max_y = bounds[1].y;
 952    h = max_y - min_y;
 953    check2 = assert(min_x>=0, "Input region must exist entirely in the X+ half-plane.");
 954    steps = segs(max_x);
 955    if (!is_undef(texture)) {
 956        _textured_revolution(
 957            shape,
 958            texture=texture,
 959            tex_size=tex_size,
 960            counts=tex_counts,
 961            tex_scale=tex_scale,
 962            inset=tex_inset,
 963            rot=tex_rot,
 964            samples=tex_samples,
 965            taper=tex_taper,
 966            shift=shift,
 967            closed=closed,
 968            angle=angle,
 969            style=style,
 970            atype=atype, anchor=anchor,
 971            spin=spin, orient=orient
 972        ) children();
 973    } else {
 974        skmat = down(min_y) * skew(sxz=shift.x/h, syz=shift.y/h) * up(min_y);
 975        transforms = [
 976            if (angle==360) for (i=[0:1:steps-1]) skmat * rot([90,0,360-i*360/steps]),
 977            if (angle<360) for (i=[0:1:steps-1]) skmat * rot([90,0,angle-i*angle/(steps-1)]),
 978        ];
 979        sweep(
 980            region, transforms,
 981            closed=angle==360,
 982            caps=angle!=360,
 983            style=style, cp=cp,
 984            convexity=convexity,
 985            atype=atype, anchor=anchor,
 986            spin=spin, orient=orient
 987        ) children();
 988    }
 989}
 990
 991
 992// Function&Module: spiral_sweep()
 993// Usage: As Module
 994//   spiral_sweep(poly, h, r|d=, turns, [taper=], [center=], [taper1=], [taper2=], [internal=], ...)[ATTACHMENTS];
 995//   spiral_sweep(poly, h, r1=|d1=, r2=|d2=, turns, [taper=], [center=], [taper1=], [taper2=], [internal=], ...)[ATTACHMENTS];
 996// Usage: As Function
 997//   vnf = spiral_sweep(poly, h, r|d=, turns, ...);
 998//   vnf = spiral_sweep(poly, h, r1=|d1=, r1=|d2=, turns, ...);
 999// Topics: Extrusion, Sweep
1000// Description:
1001//   Takes a closed 2D polygon path, centered on the XY plane, and sweeps/extrudes it along a 3D spiral path
1002//   of a given radius, height and degrees of rotation.  The origin in the profile traces out the helix of the specified radius.
1003//   If turns is positive the path will be right-handed;  if turns is negative the path will be left-handed.
1004//   .
1005//   The taper options specify tapering at of the ends of the extrusion, and are given as the linear distance
1006//   over which to taper.  If taper is positive the extrusion lengthened by the specified distance; if taper
1007//   is negative, the taper is included in the extrusion length specified by `turns`. 
1008// Arguments:
1009//   poly = Array of points of a polygon path, to be extruded.
1010//   h = height of the spiral to extrude along.
1011//   r = Radius of the spiral to extrude along.
1012//   turns = number of revolutions to spiral up along the height.
1013//   ---
1014//   d = Diameter of the spiral to extrude along.
1015//   d1|r1 = Bottom inside diameter or radius of spiral to extrude along.
1016//   d2|r2 = Top inside diameter or radius of spiral to extrude along.
1017//   taper = Length of tapers for thread ends.  Positive to add taper to threads, negative to taper within specified length.  Default: 0
1018//   taper1 = Length of taper for bottom thread end
1019//   taper2 = Length of taper for top thread end
1020//   internal = if true make internal threads.  The only effect this has is to change how the extrusion tapers if tapering is selected. When true, the extrusion tapers towards the outside; when false, it tapers towards the inside.  Default: false
1021//   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
1022//   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
1023//   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
1024//   center = If given, overrides `anchor`.  A true value sets `anchor=CENTER`, false sets `anchor=BOTTOM`.
1025// See Also: sweep(), linear_sweep(), rotate_sweep(), path_sweep()
1026// Example:
1027//   poly = [[-10,0], [-3,-5], [3,-5], [10,0], [0,-30]];
1028//   spiral_sweep(poly, h=200, r=50, turns=3, $fn=36);
1029function _taperfunc(x) =
1030     let(higofs = pow(0.05,2))   // Smallest hig scale is the square root of this value
1031     sqrt((1-higofs)*x+higofs);
1032function _taperfunc_ellipse(x) =
1033     sqrt(1-(1-x)^2);
1034function _ss_polygon_r(N,theta) =
1035        let( alpha = 360/N )
1036        cos(alpha/2)/(cos(posmod(theta,alpha)-alpha/2));
1037function spiral_sweep(poly, h, r, turns=1, taper, center, r1, r2, d, d1, d2, taper1, taper2, internal=false, anchor=CENTER, spin=0, orient=UP) =
1038    assert(is_num(turns) && turns != 0)
1039    let(
1040        tapersample = 10,         // Oversample factor for higbee tapering
1041        dir = sign(turns),
1042        r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=50),
1043        r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=50),
1044        bounds = pointlist_bounds(poly),
1045        yctr = (bounds[0].y+bounds[1].y)/2,
1046        xmin = bounds[0].x,
1047        xmax = bounds[1].x,
1048        poly = path3d(clockwise_polygon(poly)),
1049        anchor = get_anchor(anchor,center,BOT,BOT),
1050        sides = segs(max(r1,r2)),
1051        ang_step = 360/sides,
1052        turns = abs(turns),
1053        taper1 = first_defined([taper1, taper, 0]),
1054        taper2 = first_defined([taper2, taper, 0]),
1055        taperang1 = 360 * abs(taper1) / (2 * r1 * PI),
1056        taperang2 = 360 * abs(taper2) / (2 * r2 * PI),
1057        minang = taper1<=0 ? 0 : -taperang1,
1058        tapercut1 = taper1<=0 ? taperang1 : 0,
1059        maxang = taper2<=0 ? 360*turns : 360*turns+taperang2,
1060        tapercut2 = taper2<=0 ? 360*turns-taperang2 : 360*turns
1061    )
1062    assert( tapercut1<tapercut2 && tapercut1<maxang, "Tapers are too long to fit")
1063    assert( all_positive([r1,r2]), "Diameter/radius must be positive")
1064    let(
1065        // This complicated sampling scheme is designed to ensure that faceting always starts at angle zero
1066        // for alignment with cylinders, and there is always a facet boundary at the $fn specified locations, 
1067        // regardless of what kind of subsampling occurs for tapers.
1068        orig_anglist = [
1069            if (minang<0) minang,
1070            each reverse([for(ang = [-ang_step:-ang_step:minang+EPSILON]) ang]),
1071            for(ang = [0:ang_step:maxang-EPSILON]) ang,
1072            maxang
1073        ],
1074        anglist = [
1075           for(a=orig_anglist) if (a<tapercut1-EPSILON) a,
1076           tapercut1,
1077           for(a=orig_anglist) if (a>tapercut1+EPSILON && a<tapercut2-EPSILON) a,
1078           tapercut2,
1079           for(a=orig_anglist) if (a>tapercut2+EPSILON) a
1080        ],
1081        interp_ang = [
1082                      for(i=idx(anglist,e=-2))
1083                          each lerpn(anglist[i],anglist[i+1],
1084                                         (taper1!=0 && anglist[i+1]<=tapercut1) || (taper2!=0 && anglist[i]>=tapercut2)
1085                                            ? ceil((anglist[i+1]-anglist[i])/ang_step*tapersample)
1086                                            : 1,
1087                                     endpoint=false),
1088                      last(anglist)
1089                     ],
1090        e=echo(lenlist=len(interp_ang)),                     
1091        skewmat = affine3d_skew_xz(xa=atan2(r2-r1,h)),
1092        points = [
1093            for (a = interp_ang) let (
1094                hsc = a<tapercut1 ? _taperfunc((a-minang)/taperang1)
1095                    : a>tapercut2 ? _taperfunc((maxang-a)/taperang2)
1096                    : 1,
1097                u = a/(360*turns), 
1098                r = lerp(r1,r2,u),
1099                mat = affine3d_zrot(dir*a)
1100                    * affine3d_translate([_ss_polygon_r(sides,dir*a)*r, 0, dir*h * (u-0.5)])
1101                    * affine3d_xrot(90)
1102                    * skewmat
1103                    * scale([hsc,lerp(hsc,1,0.25),1], cp=[internal ? xmax : xmin, yctr, 0]),
1104                pts = apply(mat, poly)
1105            ) pts
1106        ],
1107        vnf = vnf_vertex_array(
1108            points, col_wrap=true, caps=true, reverse=dir>0,
1109        //    style=higbee1>0 || higbee2>0 ? "quincunx" : "alt"
1110            style="convex"
1111        )
1112    )
1113    reorient(anchor,spin,orient, vnf=vnf, r1=r1, r2=r2, l=h, p=vnf);
1114
1115
1116
1117module spiral_sweep(poly, h, r, turns=1, taper, center, r1, r2, d, d1, d2, taper1, taper2, internal=false, anchor=CENTER, spin=0, orient=UP) {
1118    vnf = spiral_sweep(poly, h, r, turns, taper, center, r1, r2, d, d1, d2, taper1, taper2, internal);
1119    r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=50);
1120    r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=50);
1121    taper1 = first_defined([taper1,taper,0]);
1122    taper2 = first_defined([taper2,taper,0]);
1123    extra = PI/2*(max(0,taper1/r1)+max(0,taper2/r2));
1124    attachable(anchor,spin,orient, r1=r1, r2=r2, l=h) {
1125        vnf_polyhedron(vnf, convexity=ceil(2*(abs(turns)+extra)));
1126        children();
1127    }
1128}
1129
1130
1131
1132// Function&Module: path_sweep()
1133// Usage: As module
1134//   path_sweep(shape, path, [method], [normal=], [closed=], [twist=], [twist_by_length=], [symmetry=], [scale=], [scale_by_length=], [last_normal=], [tangent=], [uniform=], [relaxed=], [caps=], [style=], [convexity=], [anchor=], [cp=], [spin=], [orient=], [atype=]) [ATTACHMENTS];
1135// Usage: As function
1136//   vnf = path_sweep(shape, path, [method], [normal=], [closed=], [twist=], [twist_by_length=], [symmetry=], [scale=], [scale_by_length=], [last_normal=], [tangent=], [uniform=], [relaxed=], [caps=], [style=], [transforms=], [anchor=], [cp=], [spin=], [orient=], [atype=]);
1137// Description:
1138//   Takes as input `shape`, a 2D polygon path (list of points), and `path`, a 2d or 3d path (also a list of points)
1139//   and constructs a polyhedron by sweeping the shape along the path. When run as a module returns the polyhedron geometry.
1140//   When run as a function returns a VNF by default or if you set `transforms=true` then it returns a list of transformations suitable as input to `sweep`.
1141//   .
1142//   The sweeping process places one copy of the shape for each point in the path.  The origin in `shape` is translated to
1143//   the point in `path`.  The normal vector of the shape, which points in the Z direction, is aligned with the tangent
1144//   vector for the path, so this process is constructing a shape whose normal cross sections are equal to your specified shape.
1145//   If you do not supply a list of tangent vectors then an approximate tangent vector is computed
1146//   based on the path points you supply using {{path_tangents()}}.
1147// Figure(3D,Big,VPR=[70,0,345],VPD=20,VPT=[5.5,10.8,-2.7],NoScales): This example shows how the shape, in this case the quadrilateral defined by `[[0, 0], [0, 1], [0.25, 1], [1, 0]]`, appears as the cross section of the swept polyhedron.  The blue line shows the path.  The normal vector to the shape is shown in black; it is based at the origin and points upwards in the Z direction.  The sweep aligns this normal vector with the blue path tangent, which in this case, flips the shape around.  Note that for a 2D path like this one, the Y direction in the shape is mapped to the Z direction in the sweep.
1148//   tri= [[0, 0], [0, 1], [.25,1], [1, 0]];
1149//   path = arc(r=5,n=81,angle=[-20,65]);
1150//   % path_sweep(tri,path);
1151//   T = path_sweep(tri,path,transforms=true);
1152//   color("red")for(i=[0:20:80]) stroke(apply(T[i],path3d(tri)),width=.1,closed=true);
1153//   color("blue")stroke(path3d(arc(r=5,n=101,angle=[-20,80])),width=.1,endcap2="arrow2");
1154//   color("red")stroke([path3d(tri)],width=.1);
1155//   stroke([CENTER,UP], width=.07,endcap2="arrow2",color="black");
1156// Continues:
1157//   In the figure you can see that the swept polyhedron, shown in transparent gray, has the quadrilateral as its cross
1158//   section.  The quadrilateral is positioned perpendicular to the path, which is shown in blue, so that the normal
1159//   vector for the quadrilateral is parallel to the tangent vector for the path.  The origin for the shape is the point
1160//   which follows the path.  For a 2D path, the Y axis of the shape is mapped to the Z axis and in this case,
1161//   pointing the quadrilateral's normal vector (in black) along the tangent line of
1162//   the path, which is going in the direction of the blue arrow, requires that the quadrilateral be "turned around".  If we
1163//   reverse the order of points in the path we get a different result:
1164// Figure(3D,Big,VPR=[70,0,20],VPD=20,VPT=[1.25,9.25,-2.65],NoScales): The same sweep operation with the path traveling in the opposite direction.  Note that in order to line up the normal correctly, the shape is reversed compared to Figure 1, so the resulting sweep looks quite different.
1165//   tri= [[0, 0], [0, 1], [.25,1], [1, 0]];
1166//   path = reverse(arc(r=5,n=81,angle=[-20,65]));
1167//   % path_sweep(tri,path);
1168//   T = path_sweep(tri,path,transforms=true);
1169//   color("red")for(i=[0:20:80]) stroke(apply(T[i],path3d(tri)),width=.1,closed=true);
1170//   color("blue")stroke(reverse(path3d(arc(r=5,n=101,angle=[-20-15,65]))),width=.1,endcap2="arrow2");
1171//   color("red")stroke([path3d(tri)],width=.1);
1172//   stroke([CENTER,UP], width=.07,endcap2="arrow2",color="black");
1173// Continues:
1174//   If your shape is too large for the curves in the path you can create a situation where the shapes cross each
1175//   other.  This results in an invalid polyhedron, which may appear OK when previewed or rendered alone, but will give rise
1176//   to cryptic CGAL errors when rendered with a second object in your model.  You may be able to use {{path_sweep2d()}}
1177//   to produce a valid model in cases like this.  You can debug models like this using the `profiles=true` option which will show all
1178//   the cross sections in your polyhedron.  If any of them intersect, the polyhedron will be invalid.
1179// Figure(3D,Big,VPR=[47,0,325],VPD=23,VPT=[6.8,4,-3.8],NoScales): We have scaled the path to an ellipse and show a large triangle as the shape.  The triangle is sometimes bigger than the local radius of the path, leading to an invalid polyhedron, which you can identify because the red lines cross in the middle.
1180//   tri= scale([4.5,2.5],[[0, 0], [0, 1], [1, 0]]);
1181//   path = xscale(1.5,arc(r=5,n=81,angle=[-70,70]));
1182//   % path_sweep(tri,path);
1183//   T = path_sweep(tri,path,transforms=true);
1184//   color("red")for(i=[0:20:80]) stroke(apply(T[i],path3d(tri)),width=.1,closed=true);
1185//   color("blue")stroke(path3d(xscale(1.5,arc(r=5,n=81,angle=[-70,80]))),width=.1,endcap2="arrow2");
1186// Continues:
1187//   During the sweep operation the shape's normal vector aligns with the tangent vector of the path.  Note that
1188//   this leaves an ambiguity about how the shape is rotated as it sweeps along the path.
1189//   For 2D paths, this ambiguity is resolved by aligning the Y axis of the shape to the Z axis of the swept polyhedron.
1190//   You can can force the  shape to twist as it sweeps along the path using the `twist` parameter, which specifies the total
1191//   number of degrees to twist along the whole swept polyhedron.  This produces a result like the one shown below.
1192// Figure(3D,Big,VPR=[66,0,14],VPD=20,VPT=[3.4,4.5,-0.8]): The shape twists as we sweep.  Note that it still aligns the origin in the shape with the path, and still aligns the normal vector with the path tangent vector.
1193//   tri= [[0, 0], [0, 1], [.25,1],[1, 0]];
1194//   path = arc(r=5,n=81,angle=[-20,65]);
1195//   % path_sweep(tri,path,twist=-60);
1196//   T = path_sweep(tri,path,transforms=true,twist=-60);
1197//   color("red")for(i=[0:20:80]) stroke(apply(T[i],path3d(tri)),width=.1,closed=true);
1198//   color("blue")stroke(path3d(arc(r=5,n=101,angle=[-20,80])),width=.1,endcap2="arrow2");
1199// Continues:
1200//   The `twist` argument adds the specified number of degrees of twist into the model, and it may be positive or
1201//   negative.  When `closed=true` the starting shape and ending shape must match to avoid a sudden extreme twist at the
1202//   joint.  By default `twist` is therefore required to be a multiple of 360.  However, if your shape has rotational
1203//   symmetry, this requirement is overly strict.  You can specify the symmetry using the `symmetry` argument, and then
1204//   you can choose smaller twists consistent with the specified symmetry.  The symmetry argument gives the number of
1205//   rotations that map the shape exactly onto itself, so a pentagon has 5-fold symmetry.  This argument is only valid
1206//   for closed sweeps.  When you specify symmetry, the twist must be a multiple of 360/symmetry.
1207//   .
1208//   The twist is normally spread uniformly along your shape based on the path length.  If you set `twist_by_length` to
1209//   false then the twist will be uniform based on the point count of your path.  Twisted shapes will produce twisted
1210//   faces, so if you want them to look good you should use lots of points on your path and also lots of points on the
1211//   shape.  If your shape is a simple polygon, use {{subdivide_path()}} to increase
1212//   the number of points.
1213//   .
1214//   As noted above, the sweep process has an ambiguity regarding the twist.  For 2D paths it is easy to resolve this
1215//   ambiguity by aligning the Y axis in the shape to the Z axis in the swept polyhedron.  When the path is
1216//   three-dimensional, things become more complex.  It is no longer possible to use a simple alignment rule like the
1217//   one we use in 2D.  You may find that the shape rotates unexpectedly around its axis as it traverses the path.  The
1218//   `method` parameter allows you to specify how the shapes are aligned, resulting in different twist in the resulting
1219//   polyhedron.  You can choose from three different methods for selecting the rotation of your shape.  None of these
1220//   methods will produce good, or even valid, results on all inputs, so it is important to select a suitable method.
1221//   .
1222//   The three methods you can choose using the `method` parameter are:
1223//   .
1224//   The "incremental" method (the default) works by adjusting the shape at each step by the minimal rotation that makes the shape normal to the tangent
1225//   at the next point.  This method is robust in that it always produces a valid result for well-behaved paths with sufficiently high
1226//   sampling.  Unfortunately, it can produce a large amount of undesirable twist.  When constructing a closed shape this algorithm in
1227//   its basic form provides no guarantee that the start and end shapes match up.  To prevent a sudden twist at the last segment,
1228//   the method calculates the required twist for a good match and distributes it over the whole model (as if you had specified a
1229//   twist amount).  If you specify `symmetry` this may allow the algorithm to choose a smaller twist for this alignment.
1230//   To start the algorithm, we need an initial condition.  This is supplied by
1231//   using the `normal` argument to give a direction to align the Y axis of your shape.  By default the normal points UP if the path
1232//   makes an angle of 45 deg or less with the xy plane and it points BACK if the path makes a higher angle with the XY plane.  You
1233//   can also supply `last_normal` which provides an ending orientation constraint.  Be aware that the curve may still exhibit
1234//   twisting in the middle.  This method is the default because it is the most robust, not because it generally produces the best result.
1235//   .
1236//   The "natural" method works by computing the Frenet frame at each point on the path.  This is defined by the tangent to the curve and
1237//   the normal which lies in the plane defined by the curve at each point.  This normal points in the direction of curvature of the curve.
1238//   The result is a very well behaved set of shape positions without any unexpected twisting&mdash;as long as the curvature never falls to zero.  At a
1239//   point of zero curvature (a flat point), the curve does not define a plane and the natural normal is not defined.  Furthermore, even if
1240//   you skip over this troublesome point so the normal is defined, it can change direction abruptly when the curvature is zero, leading to
1241//   a nasty twist and an invalid model.  A simple example is a circular arc joined to another arc that curves the other direction.  Note
1242//   that the X axis of the shape is aligned with the normal from the Frenet frame.
1243//   .
1244//   The "manual" method allows you to specify your desired normal either globally with a single vector, or locally with
1245//   a list of normal vectors for every path point.  The normal you supply is projected to be orthogonal to the tangent to the
1246//   path and the Y direction of your shape will be aligned with the projected normal.  (Note this is different from the "natural" method.)
1247//   Careless choice of a normal may result in a twist in the shape, or an error if your normal is parallel to the path tangent.
1248//   If you set `relax=true` then the condition that the cross sections are orthogonal to the path is relaxed and the swept object
1249//   uses the actual specified normal.  In this case, the tangent is projected to be orthogonal to your supplied normal to define
1250//   the cross section orientation.  Specifying a list of normal vectors gives you complete control over the orientation of your
1251//   cross sections and can be useful if you want to position your model to be on the surface of some solid.
1252//   .
1253//   You can also apply scaling to the profile along the path.  You can give a list of scalar scale factors or a list of 2-vector scale. 
1254//   In the latter scale the x and y scales of the profile are scaled separately before the profile is placed onto the path.  For non-closed
1255//   paths you can also give a single scale value or a 2-vector which is treated as the final scale.  The intermediate sections
1256//   are then scaled by linear interpolation either relative to length (if scale_by_length is true) or by point count otherwise.  
1257//   .
1258//   You can use set `transforms` to true to return a list of transformation matrices instead of the swept shape.  In this case, you can
1259//   often omit shape entirely.  The exception is when `closed=true` and you are using the "incremental" method.  In this case, `path_sweep`
1260//   uses the shape to correct for twist when the shape closes on itself, so you must include a valid shape.
1261// Arguments:
1262//   shape = A 2D polygon path or region describing the shape to be swept.
1263//   path = 2D or 3D path giving the path to sweep over
1264//   method = one of "incremental", "natural" or "manual".  Default: "incremental"
1265//   ---
1266//   normal = normal vector for initializing the incremental method, or for setting normals with method="manual".  Default: UP if the path makes an angle lower than 45 degrees to the xy plane, BACK otherwise.
1267//   closed = path is a closed loop.  Default: false
1268//   twist = amount of twist to add in degrees.  For closed sweeps must be a multiple of 360/symmetry.  Default: 0
1269//   twist_by_length = if true then interpolate twist based on the path length of the path. If false interoplate based on point count.  Default: true
1270//   symmetry = symmetry of the shape when closed=true.  Allows the shape to join with a 360/symmetry rotation instead of a full 360 rotation.  Default: 1
1271//   scale = Amount to scale the profiles.  If you give a scalar the scale starts at 1 and ends at your specified value. The same is true for a 2-vector, but x and y are scaled separately.   You can also give a vector of values, one for each path point, and you can give a list of 2-vectors that give the x and y scales of your profile for every point on the path (a Nx2 matrix for a path of length N.  Default: 1 (no scaling)
1272//   scale_by_length = if true then interpolate scale based on the path length of the path. If false interoplate based on point count.  Default: true
1273//   last_normal = normal to last point in the path for the "incremental" method.  Constrains the orientation of the last cross section if you supply it.
1274//   uniform = if set to false then compute tangents using the uniform=false argument, which may give better results when your path is non-uniformly sampled.  This argument is passed to {{path_tangents()}}.  Default: true
1275//   tangent = a list of tangent vectors in case you need more accuracy (particularly at the end points of your curve)
1276//   relaxed = set to true with the "manual" method to relax the orthogonality requirement of cross sections to the path tangent.  Default: false
1277//   caps = Can be a boolean or vector of two booleans.  Set to false to disable caps at the two ends.  Default: true
1278//   style = vnf_vertex_array style.  Default: "min_edge"
1279//   profiles = if true then display all the cross section profiles instead of the solid shape.  Can help debug a sweep.  (module only) Default: false
1280//   width = the width of lines used for profile display.  (module only) Default: 1
1281//   transforms = set to true to return transforms instead of a VNF.  These transforms can be manipulated and passed to sweep().  (function only)  Default: false.
1282//   convexity = convexity parameter for polyhedron().  (module only)  Default: 10
1283//   anchor = Translate so anchor point is at the origin. Default: "origin"
1284//   spin = Rotate this many degrees around Z axis after anchor. Default: 0
1285//   orient = Vector to rotate top towards after spin
1286//   atype  = Select "hull" or "intersect" anchor types.  Default: "hull"
1287//   cp = Centerpoint for determining "intersect" anchors or centering the shape.  Determintes the base of the anchor vector.  Can be "centroid", "mean", "box" or a 3D point.  Default: "centroid"
1288// Anchor Types:
1289//   "hull" = Anchors to the virtual convex hull of the shape.
1290//   "intersect" = Anchors to the surface of the shape.
1291// See Also: sweep(), linear_sweep(), rotate_sweep(), spiral_sweep()
1292// Example(NoScales): A simple sweep of a square along a sine wave:
1293//   path = [for(theta=[-180:5:180]) [theta/10, 10*sin(theta)]];
1294//   sq = square(6,center=true);
1295//   path_sweep(sq,path);
1296// Example(NoScales): If the square is not centered, then we get a different result because the shape is in a different place relative to the origin:
1297//   path = [for(theta=[-180:5:180]) [theta/10, 10*sin(theta)]];
1298//   sq = square(6);
1299//   path_sweep(sq,path);
1300// Example(Med,VPR=[34,0,8],NoScales): It may not be obvious, but the polyhedron in the previous example is invalid.  It will eventually give CGAL errors when you combine it with other shapes.  To see this, set profiles to true and look at the left side.  The profiles cross each other and intersect.  Any time this happens, your polyhedron is invalid, even if it seems to be working at first.  Another observation from the profile display is that we have more profiles than needed over a lot of the shape, so if the model is slow, using fewer profiles in the flat portion of the curve might speed up the calculation.
1301//   path = [for(theta=[-180:5:180]) [theta/10, 10*sin(theta)]];
1302//   sq = square(6);
1303//   path_sweep(sq,path,profiles=true,width=.1,$fn=8);
1304// Example(2D): We'll use this shape in several examples
1305//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1306//   polygon(ushape);
1307// Example(NoScales): Sweep along a clockwise elliptical arc, using default "incremental" method.
1308//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1309//   elliptic_arc = xscale(2, p=arc($fn=64,angle=[180,00], r=30));  // Clockwise
1310//   path_sweep(ushape, path3d(elliptic_arc));
1311// Example(NoScales): Sweep along a counter-clockwise elliptical arc.  Note that the orientation of the shape flips.
1312//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1313//   elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=30));   // Counter-clockwise
1314//   path_sweep(ushape, path3d(elliptic_arc));
1315// Example(NoScales): Sweep along a clockwise elliptical arc, using "natural" method, which lines up the X axis of the shape with the direction of curvature.  This means the X axis will point inward, so a counterclockwise arc gives:
1316//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1317//   elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=30));  // Counter-clockwise
1318//   path_sweep(ushape, elliptic_arc, method="natural");
1319// Example(NoScales): Sweep along a clockwise elliptical arc, using "natural" method.  If the curve is clockwise then the shape flips upside-down to align the X axis.
1320//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1321//   elliptic_arc = xscale(2, p=arc($fn=64,angle=[180,0], r=30));  // Clockwise
1322//   path_sweep(ushape, path3d(elliptic_arc), method="natural");
1323// Example(NoScales): Sweep along a clockwise elliptical arc, using "manual" method.  You can orient the shape in a direction you choose (subject to the constraint that the profiles remain normal to the path):
1324//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1325//   elliptic_arc = xscale(2, p=arc($fn=64,angle=[180,0], r=30));  // Clockwise
1326//   path_sweep(ushape, path3d(elliptic_arc), method="manual", normal=UP+RIGHT);
1327// Example(NoScales): Here we changed the ellipse to be more pointy, and with the same results as above we get a shape with an irregularity in the middle where it maintains the specified direction around the point of the ellipse.  If the ellipse were more pointy, this would result in a bad polyhedron:
1328//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1329//   elliptic_arc = yscale(2, p=arc($fn=64,angle=[180,0], r=30));  // Clockwise
1330//   path_sweep(ushape, path3d(elliptic_arc), method="manual", normal=UP+RIGHT);
1331// Example(NoScales): It is easy to produce an invalid shape when your path has a smaller radius of curvature than the width of your shape.  The exact threshold where the shape becomes invalid depends on the density of points on your path.  The error may not be immediately obvious, as the swept shape appears fine when alone in your model, but adding a cube to the model reveals the problem.  In this case the pentagon is turned so its longest direction points inward to create the singularity.
1332//   qpath = [for(x=[-3:.01:3]) [x,x*x/1.8,0]];
1333//   // Prints 0.9, but we use pentagon with radius of 1.0 > 0.9
1334//   echo(radius_of_curvature = 1/max(path_curvature(qpath)));
1335//   path_sweep(apply(rot(90),pentagon(r=1)), qpath, normal=BACK, method="manual");
1336//   cube(0.5);    // Adding a small cube forces a CGAL computation which reveals
1337//                 // the error by displaying nothing or giving a cryptic message
1338// Example(NoScales): Using the `relax` option we allow the profiles to deviate from orthogonality to the path.  This eliminates the crease that broke the previous example because the sections are all parallel to each other.
1339//   qpath = [for(x=[-3:.01:3]) [x,x*x/1.8,0]];
1340//   path_sweep(apply(rot(90),pentagon(r=1)), qpath, normal=BACK, method="manual", relaxed=true);
1341//   cube(0.5);    // Adding a small cube is not a problem with this valid model
1342// Example(Med,VPR=[16,0,100],VPT=[0.05,0.6,0.6],VPD=25,NoScales): Using the `profiles=true` option can help debug bad polyhedra such as this one.  If any of the profiles intersect or cross each other, the polyhedron will be invalid.  In this case, you can see these intersections in the middle of the shape, which may give insight into how to fix your shape.   The profiles may also help you identify cases with a valid polyhedron where you have more profiles than needed to adequately define the shape.
1343//   tri= scale([4.5,2.5],[[0, 0], [0, 1], [1, 0]]);
1344//   path = left(4,xscale(1.5,arc(r=5,n=25,angle=[-70,70])));
1345//   path_sweep(tri,path,profiles=true,width=.1);
1346// Example(NoScales):  This 3d arc produces a result that twists to an undefined angle.  By default the incremental method sets the starting normal to UP, but the ending normal is unconstrained.
1347//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1348//   arc = yrot(37, p=path3d(arc($fn=64, r=30, angle=[0,180])));
1349//   path_sweep(ushape, arc, method="incremental");
1350// Example(NoScales): You can constrain the last normal as well.  Here we point it right, which produces a nice result.
1351//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1352//   arc = yrot(37, p=path3d(arc($fn=64, r=30, angle=[0,180])));
1353//   path_sweep(ushape, arc, method="incremental", last_normal=RIGHT);
1354// Example(NoScales): Here we constrain the last normal to UP.  Be aware that the behavior in the middle is unconstrained.
1355//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1356//   arc = yrot(37, p=path3d(arc($fn=64, r=30, angle=[0,180])));
1357//   path_sweep(ushape, arc, method="incremental", last_normal=UP);
1358// Example(NoScales): The "natural" method produces a very different result
1359//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1360//   arc = yrot(37, p=path3d(arc($fn=64, r=30, angle=[0,180])));
1361//   path_sweep(ushape, arc, method="natural");
1362// Example(NoScales): When the path starts at an angle of more that 45 deg to the xy plane the initial normal for "incremental" is BACK.  This produces the effect of the shape rising up out of the xy plane.  (Using UP for a vertical path is invalid, hence the need for a split in the defaults.)
1363//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1364//   arc = xrot(75, p=path3d(arc($fn=64, r=30, angle=[0,180])));
1365//   path_sweep(ushape, arc, method="incremental");
1366// Example(NoScales): Adding twist
1367//   // Counter-clockwise
1368//   elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=3));
1369//   path_sweep(pentagon(r=1), path3d(elliptic_arc), twist=72);
1370// Example(NoScales): Closed shape
1371//   ellipse = xscale(2, p=circle($fn=64, r=3));
1372//   path_sweep(pentagon(r=1), path3d(ellipse), closed=true);
1373// Example(NoScales): Closed shape with added twist
1374//   ellipse = xscale(2, p=circle($fn=64, r=3));
1375//   // Looks better with finer sampling
1376//   pentagon = subdivide_path(pentagon(r=1), 30);
1377//   path_sweep(pentagon, path3d(ellipse),
1378//              closed=true, twist=360);
1379// Example(NoScales): The last example was a lot of twist.  In order to use less twist you have to tell `path_sweep` that your shape has symmetry, in this case 5-fold.  Mobius strip with pentagon cross section:
1380//   ellipse = xscale(2, p=circle($fn=64, r=3));
1381//   // Looks better with finer sampling
1382//   pentagon = subdivide_path(pentagon(r=1), 30);
1383//   path_sweep(pentagon, path3d(ellipse), closed=true,
1384//              symmetry = 5, twist=2*360/5);
1385// Example(Med,NoScales): A helical path reveals the big problem with the "incremental" method: it can introduce unexpected and extreme twisting.  (Note helix example came from list-comprehension-demos)
1386//   function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1387//                        (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1388//                         200 * (1 - t)];
1389//   helix_steps = 200;
1390//   helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1391//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1392//   path_sweep(ushape, helix);
1393// Example(Med,NoScales): You can constrain both ends, but still the twist remains:
1394//   function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1395//                        (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1396//                         200 * (1 - t)];
1397//   helix_steps = 200;
1398//   helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1399//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1400//   path_sweep(ushape, helix, normal=UP, last_normal=UP);
1401// Example(Med,NoScales): Even if you manually guess the amount of twist and remove it, the result twists one way and then the other:
1402//   function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1403//                        (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1404//                         200 * (1 - t)];
1405//   helix_steps = 200;
1406//   helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1407//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1408//   path_sweep(ushape, helix, normal=UP, last_normal=UP, twist=360);
1409// Example(Med,NoScales): To get a good result you must use a different method.
1410//   function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1411//                        (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1412//                         200 * (1 - t)];
1413//   helix_steps = 200;
1414//   helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1415//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1416//   path_sweep(ushape, helix, method="natural");
1417// Example(Med,NoScales): Note that it may look like the shape above is flat, but the profiles are very slightly tilted due to the nonzero torsion of the curve.  If you want as flat as possible, specify it so with the "manual" method:
1418//   function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1419//                        (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1420//                         200 * (1 - t)];
1421//   helix_steps = 200;
1422//   helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1423//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1424//   path_sweep(ushape, helix, method="manual", normal=UP);
1425// Example(Med,NoScales): What if you want to angle the shape inward?  This requires a different normal at every point in the path:
1426//   function helix(t) = [(t / 1.5 + 0.5) * 30 * cos(6 * 360 * t),
1427//                        (t / 1.5 + 0.5) * 30 * sin(6 * 360 * t),
1428//                         200 * (1 - t)];
1429//   helix_steps = 200;
1430//   helix = [for (i=[0:helix_steps]) helix(i/helix_steps)];
1431//   normals = [for(i=[0:helix_steps]) [-cos(6*360*i/helix_steps), -sin(6*360*i/helix_steps), 2.5]];
1432//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1433//   path_sweep(ushape, helix, method="manual", normal=normals);
1434// Example(NoScales): When using "manual" it is important to choose a normal that works for the whole path, producing a consistent result.  Here we have specified an upward normal, and indeed the shape is pointed up everywhere, but two abrupt transitional twists render the model invalid.
1435//   yzcircle = yrot(90,p=path3d(circle($fn=64, r=30)));
1436//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1437//   path_sweep(ushape, yzcircle, method="manual", normal=UP, closed=true);
1438// Example(NoScales): The "natural" method will introduce twists when the curvature changes direction.  A warning is displayed.
1439//   arc1 = path3d(arc(angle=90, r=30));
1440//   arc2 = xrot(-90, cp=[0,30],p=path3d(arc(angle=[90,180], r=30)));
1441//   two_arcs = path_merge_collinear(concat(arc1,arc2));
1442//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1443//   path_sweep(ushape, two_arcs, method="natural");
1444// Example(NoScales): The only simple way to get a good result is the "incremental" method:
1445//   arc1 = path3d(arc(angle=90, r=30));
1446//   arc2 = xrot(-90, cp=[0,30],p=path3d(arc(angle=[90,180], r=30)));
1447//   arc3 = apply( translate([-30,60,30])*yrot(90), path3d(arc(angle=[270,180], r=30)));
1448//   three_arcs = path_merge_collinear(concat(arc1,arc2,arc3));
1449//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1450//   path_sweep(ushape, three_arcs, method="incremental");
1451// Example(Med,NoScales): knot example from list-comprehension-demos, "incremental" method
1452//   function knot(a,b,t) =   // rolling knot
1453//        [ a * cos (3 * t) / (1 - b* sin (2 *t)),
1454//          a * sin( 3 * t) / (1 - b* sin (2 *t)),
1455//        1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))];
1456//   a = 0.8; b = sqrt (1 - a * a);
1457//   ksteps = 400;
1458//   knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)];
1459//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1460//   path_sweep(ushape, knot_path, closed=true, method="incremental");
1461// Example(Med,NoScales): knot example from list-comprehension-demos, "natural" method.  Which one do you like better?
1462//   function knot(a,b,t) =   // rolling knot
1463//        [ a * cos (3 * t) / (1 - b* sin (2 *t)),
1464//          a * sin( 3 * t) / (1 - b* sin (2 *t)),
1465//        1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))];
1466//   a = 0.8; b = sqrt (1 - a * a);
1467//   ksteps = 400;
1468//   knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)];
1469//   ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1470//   path_sweep(ushape, knot_path, closed=true, method="natural");
1471// Example(Med,NoScales): knot with twist.  Note if you twist it the other direction the center section untwists because of the natural twist there.  Also compare to the "incremental" method which has less twist in the center.
1472//   function knot(a,b,t) =   // rolling knot
1473//        [ a * cos (3 * t) / (1 - b* sin (2 *t)),
1474//          a * sin( 3 * t) / (1 - b* sin (2 *t)),
1475//        1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))];
1476//   a = 0.8; b = sqrt (1 - a * a);
1477//   ksteps = 400;
1478//   knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)];
1479//   path_sweep(subdivide_path(pentagon(r=12),30), knot_path, closed=true,
1480//              twist=-360*8, symmetry=5, method="natural");
1481// Example(Med,NoScales): twisted knot with twist distributed by path sample points instead of by length using `twist_by_length=false`
1482//   function knot(a,b,t) =   // rolling knot
1483//           [ a * cos (3 * t) / (1 - b* sin (2 *t)),
1484//             a * sin( 3 * t) / (1 - b* sin (2 *t)),
1485//           1.8 * b * cos (2 * t) /(1 - b* sin (2 *t))];
1486//   a = 0.8; b = sqrt (1 - a * a);
1487//   ksteps = 400;
1488//   knot_path = [for (i=[0:ksteps-1]) 50 * knot(a,b,(i/ksteps)*360)];
1489//   path_sweep(subdivide_path(pentagon(r=12),30), knot_path, closed=true,
1490//              twist=-360*8, symmetry=5, method="natural", twist_by_length=false);
1491// Example(Big,NoScales): This torus knot example comes from list-comprehension-demos.  The knot lies on the surface of a torus.  When we use the "natural" method the swept figure is angled compared to the surface of the torus because the curve doesn't follow geodesics of the torus.
1492//   function knot(phi,R,r,p,q) =
1493//       [ (r * cos(q * phi) + R) * cos(p * phi),
1494//         (r * cos(q * phi) + R) * sin(p * phi),
1495//          r * sin(q * phi) ];
1496//   ushape = 3*[[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1497//   points = 50;       // points per loop
1498//   R = 400; r = 150;  // Torus size
1499//   p = 2;  q = 5;     // Knot parameters
1500//   %torus(r_maj=R,r_min=r);
1501//   k = max(p,q) / gcd(p,q) * points;
1502//   knot_path   = [ for (i=[0:k-1]) knot(360*i/k/gcd(p,q),R,r,p,q) ];
1503//   path_sweep(rot(90,p=ushape),knot_path,  method="natural", closed=true);
1504// Example(Big,NoScales): By computing the normal to the torus at the path we can orient the path to lie on the surface of the torus:
1505//   function knot(phi,R,r,p,q) =
1506//       [ (r * cos(q * phi) + R) * cos(p * phi),
1507//         (r * cos(q * phi) + R) * sin(p * phi),
1508//          r * sin(q * phi) ];
1509//   function knot_normal(phi,R,r,p,q) =
1510//       knot(phi,R,r,p,q)
1511//           - R*unit(knot(phi,R,r,p,q)
1512//               - [0,0, knot(phi,R,r,p,q)[2]]) ;
1513//   ushape = 3*[[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[  7, 2],[  7, 7],[ 10, 7],[ 10, 0]];
1514//   points = 50;       // points per loop
1515//   R = 400; r = 150;  // Torus size
1516//   p = 2;  q = 5;     // Knot parameters
1517//   %torus(r_maj=R,r_min=r);
1518//   k = max(p,q) / gcd(p,q) * points;
1519//   knot_path   = [ for (i=[0:k-1]) knot(360*i/k/gcd(p,q),R,r,p,q) ];
1520//   normals = [ for (i=[0:k-1]) knot_normal(360*i/k/gcd(p,q),R,r,p,q) ];
1521//   path_sweep(ushape,knot_path,normal=normals, method="manual", closed=true);
1522// Example(NoScales): You can request the transformations and manipulate them before passing them on to sweep.  Here we construct a tube that changes scale by first generating the transforms and then applying the scale factor and connecting the inside and outside.  Note that the wall thickness varies because it is produced by scaling.
1523//   shape = star(n=5, r=10, ir=5);
1524//   rpath = arc(25, points=[[29,6,-4], [3,4,6], [1,1,7]]);
1525//   trans = path_sweep(shape, rpath, transforms=true);
1526//   outside = [for(i=[0:len(trans)-1]) trans[i]*scale(lerp(1,1.5,i/(len(trans)-1)))];
1527//   inside = [for(i=[len(trans)-1:-1:0]) trans[i]*scale(lerp(1.1,1.4,i/(len(trans)-1)))];
1528//   sweep(shape, concat(outside,inside),closed=true);
1529// Example(NoScales): An easier way to scale your model is to use the scale parameter.
1530//   elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=3));
1531//   path_sweep(pentagon(r=1), path3d(elliptic_arc), scale=2);
1532// Example(NoScales): Scaling only in the y direction of the profile (z direction in the model in this case)
1533//   elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=3));
1534//   path_sweep(rect(2), path3d(elliptic_arc), scale=[1,2]);
1535// Example(NoScales): Specifying scale at every point for a closed path
1536//   N=64;
1537//   path = circle(r=5, $fn=64);
1538//   theta = lerpn(0,360,N,endpoint=false);
1539//   scale = [for(t=theta) sin(6*t)/5+1];
1540//   path_sweep(rect(2), path3d(path), closed=true, scale=scale);
1541// Example(Med,NoScales): Using path_sweep on a region
1542//   rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)];
1543//   rgn2 = [square(30,center=false)];
1544//   rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))];
1545//   mrgn = union(rgn1,rgn2);
1546//   orgn = difference(mrgn,rgn3);
1547//   path_sweep(orgn,arc(r=40,angle=180));
1548// Example(Med,NoScales): A region with a twist
1549//   region = [for(i=pentagon(5)) move(i,p=circle(r=2,$fn=25))];
1550//   path_sweep(region,
1551//              circle(r=16,$fn=75),closed=true,
1552//              twist=360/5*2,symmetry=5);
1553// Example(Med,NoScales): Cutting a cylinder with a curved path.  Note that in this case, the incremental method produces just a slight twist but the natural method produces an extreme twist.  But manual specification produces no twist, as desired:
1554//   $fn=90;
1555//   r=8;
1556//   thickness=1;
1557//   len=21;
1558//   curve = [for(theta=[0:4:359])
1559//              [r*cos(theta), r*sin(theta), 10+sin(6*theta)]];
1560//   difference(){
1561//     cylinder(r=r, l=len);
1562//     down(.5)cylinder(r=r-thickness, l=len+1);
1563//     path_sweep(left(.05,square([1.1,1])), curve, closed=true,
1564//                method="manual", normal=UP);
1565//   }
1566
1567module path_sweep(shape, path, method="incremental", normal, closed, twist=0, twist_by_length=true, scale=1, scale_by_length=true,
1568                    symmetry=1, last_normal, tangent, uniform=true, relaxed=false, caps, style="min_edge", convexity=10,
1569                    anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull",profiles=false,width=1)
1570{
1571    dummy = assert(is_region(shape) || is_path(shape,2), "shape must be a 2D path or region");
1572    vnf = path_sweep(shape, path, method, normal, closed, twist, twist_by_length, scale, scale_by_length,
1573                    symmetry, last_normal, tangent, uniform, relaxed, caps, style);
1574
1575    if (profiles){
1576        assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"");
1577        tran = path_sweep(shape, path, method, normal, closed, twist, twist_by_length, scale, scale_by_length, 
1578                          symmetry, last_normal, tangent, uniform, relaxed,transforms=true);
1579        rshape = is_path(shape) ? [path3d(shape)]
1580                                : [for(s=shape) path3d(s)];
1581        attachable(anchor,spin,orient, vnf=vnf, extent=atype=="hull", cp=cp) {
1582            for(T=tran) stroke([for(part=rshape)apply(T,part)],width=width);
1583            children();
1584        }
1585    }
1586    else
1587      vnf_polyhedron(vnf,convexity=convexity,anchor=anchor, spin=spin, orient=orient, atype=atype, cp=cp)
1588          children();
1589}
1590
1591
1592function path_sweep(shape, path, method="incremental", normal, closed, twist=0, twist_by_length=true, scale=1, scale_by_length=true, 
1593                    symmetry=1, last_normal, tangent, uniform=true, relaxed=false, caps, style="min_edge", transforms=false,
1594                    anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull") =
1595  is_1region(path) ? path_sweep(shape=shape,path=path[0], method=method, normal=normal, closed=default(closed,true), 
1596                                twist=twist, scale=scale, scale_by_length=scale_by_length, twist_by_length=twist_by_length, symmetry=symmetry, last_normal=last_normal,
1597                                tangent=tangent, uniform=uniform, relaxed=relaxed, caps=caps, style=style, transforms=transforms,
1598                                anchor=anchor, cp=cp, spin=spin, orient=orient, atype=atype) :
1599  let(closed=default(closed,false))
1600  assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"")
1601  assert(!closed || twist % (360/symmetry)==0, str("For a closed sweep, twist must be a multiple of 360/symmetry = ",360/symmetry))
1602  assert(closed || symmetry==1, "symmetry must be 1 when closed is false")
1603  assert(is_integer(symmetry) && symmetry>0, "symmetry must be a positive integer")
1604  let(path = force_path(path))
1605  assert(is_path(path,[2,3]), "input path is not a 2D or 3D path")
1606  assert(!closed || !approx(path[0],last(path)), "Closed path includes start point at the end")
1607  assert((is_region(shape) || is_path(shape,2)) || (transforms && !(closed && method=="incremental")),"shape must be a 2d path or region")
1608  let(
1609    path = path3d(path),
1610    caps = is_def(caps) ? caps :
1611           closed ? false : true,
1612    capsOK = is_bool(caps) || is_bool_list(caps,2),
1613    fullcaps = is_bool(caps) ? [caps,caps] : caps,
1614    normalOK = is_undef(normal) || (method!="natural" && is_vector(normal,3))
1615                                || (method=="manual" && same_shape(normal,path)),
1616    scaleOK = scale==1 || ((is_num(scale) || is_vector(scale,2)) && !closed) || is_vector(scale,len(path)) || is_matrix(scale,len(path),2)
1617    
1618  )
1619  assert(normalOK,  method=="natural" ? "Cannot specify normal with the \"natural\" method"
1620                  : method=="incremental" ? "Normal with \"incremental\" method must be a 3-vector"
1621                  : str("Incompatible normal given.  Must be a 3-vector or a list of ",len(path)," 3-vectors"))
1622  assert(capsOK, "caps must be boolean or a list of two booleans")
1623  assert(!closed || !caps, "Cannot make closed shape with caps")
1624  assert(is_undef(normal) || (is_vector(normal) && len(normal)==3) || (is_path(normal) && len(normal)==len(path) && len(normal[0])==3), "Invalid normal specified")
1625  assert(is_undef(tangent) || (is_path(tangent) && len(tangent)==len(path) && len(tangent[0])==3), "Invalid tangent specified")
1626  assert(scaleOK,str("Incompatible or invalid scale",closed?" for closed path":"",": must be ", closed?"":"a scalar, a 2-vector, ",
1627                     "a vector of length ",len(path)," or a ",len(path),"x2 matrix of scales"))
1628  let(
1629    scale = !(is_num(scale) || is_vector(scale,2)) ? scale
1630          : let(s=is_num(scale) ? [scale,scale] : scale)
1631            !scale_by_length ? lerpn([1,1],s,len(path))
1632          : lerp([1,1],s, path_length_fractions(path,false)),
1633    scale_list = [for(s=scale) scale(s),if (closed) scale(scale[0])],
1634    tangents = is_undef(tangent) ? path_tangents(path,uniform=uniform,closed=closed) : [for(t=tangent) unit(t)],
1635    normal = is_path(normal) ? [for(n=normal) unit(n)] :
1636             is_def(normal) ? unit(normal) :
1637             method =="incremental" && abs(tangents[0].z) > 1/sqrt(2) ? BACK : UP,
1638    normals = is_path(normal) ? normal : repeat(normal,len(path)),
1639    tpathfrac = twist_by_length ? path_length_fractions(path, closed) : [for(i=[0:1:len(path)]) i / (len(path)-(closed?0:1))],
1640    spathfrac = scale_by_length ? path_length_fractions(path, closed) : [for(i=[0:1:len(path)]) i / (len(path)-(closed?0:1))],    
1641    L = len(path),
1642    unscaled_transform_list =
1643        method=="incremental" ?
1644          let(rotations =
1645                 [for( i  = 0,
1646                       ynormal = normal - (normal * tangents[0])*tangents[0],
1647                       rotation = frame_map(y=ynormal, z=tangents[0])
1648                         ;
1649                       i < len(tangents) + (closed?1:0) ;
1650                       rotation = i<len(tangents)-1+(closed?1:0)? rot(from=tangents[i],to=tangents[(i+1)%L])*rotation : undef,
1651                       i=i+1
1652                      )
1653                   rotation],
1654              // The mismatch is the inverse of the last transform times the first one for the closed case, or the inverse of the
1655              // desired final transform times the realized final transform in the open case.  Note that when closed==true the last transform
1656              // is a actually looped around and applies to the first point position, so if we got back exactly where we started
1657              // then it will be the identity, but we might have accumulated some twist which will show up as a rotation around the
1658              // X axis.  Similarly, in the closed==false case the desired and actual transformations can only differ in the twist,
1659              // so we can need to calculate the twist angle so we can apply a correction, which we distribute uniformly over the whole path.
1660              reference_rot = closed ? rotations[0] :
1661                           is_undef(last_normal) ? last(rotations) :
1662                             let(
1663                                 last_tangent = last(tangents),
1664                                 lastynormal = last_normal - (last_normal * last_tangent) * last_tangent
1665                             )
1666                           frame_map(y=lastynormal, z=last_tangent),
1667              mismatch = transpose(last(rotations)) * reference_rot,
1668              correction_twist = atan2(mismatch[1][0], mismatch[0][0]),
1669              // Spread out this extra twist over the whole sweep so that it doesn't occur
1670              // abruptly as an artifact at the last step.
1671              twistfix = correction_twist%(360/symmetry),
1672              adjusted_final = !closed ? undef :
1673                            translate(path[0]) * rotations[0] * zrot(-correction_twist+correction_twist%(360/symmetry)-twist)
1674          )  [for(i=idx(path)) translate(path[i]) * rotations[i] * zrot((twistfix-twist)*tpathfrac[i]), if(closed) adjusted_final] 
1675      : method=="manual" ?
1676              [for(i=[0:L-(closed?0:1)]) let(
1677                       ynormal = relaxed ? normals[i%L] : normals[i%L] - (normals[i%L] * tangents[i%L])*tangents[i%L],
1678                       znormal = relaxed ? tangents[i%L] - (normals[i%L] * tangents[i%L])*normals[i%L] : tangents[i%L],
1679                       rotation = frame_map(y=ynormal, z=znormal)
1680                   )
1681                   assert(approx(ynormal*znormal,0),str("Supplied normal is parallel to the path tangent at point ",i))
1682                   translate(path[i%L])*rotation*zrot(-twist*tpathfrac[i])
1683              ] 
1684      : method=="natural" ?   // map x axis of shape to the path normal, which points in direction of curvature
1685              let (pathnormal = path_normals(path, tangents, closed))
1686              assert(all_defined(pathnormal),"Natural normal vanishes on your curve, select a different method")
1687              let( testnormals = [for(i=[0:len(pathnormal)-1-(closed?1:2)]) pathnormal[i]*select(pathnormal,i+2)],
1688                   a=[for(i=idx(testnormals)) testnormals[i]<.5 ? echo(str("Big change at index ",i," pn=",pathnormal[i]," pn2= ",select(pathnormal,i+2))):0],
1689                   dummy = min(testnormals) < .5 ? echo("WARNING: ***** Abrupt change in normal direction.  Consider a different method in path_sweep() *****") :0
1690                 )
1691              [for(i=[0:L-(closed?0:1)]) let(
1692                       rotation = frame_map(x=pathnormal[i%L], z=tangents[i%L])
1693                   )
1694                   translate(path[i%L])*rotation*zrot(-twist*tpathfrac[i])
1695                 ] 
1696      : assert(false,"Unknown method or no method given"), // unknown method
1697    transform_list = v_mul(unscaled_transform_list, scale_list),
1698    ends_match = !closed ? true
1699                 : let( rshape = is_path(shape) ? [path3d(shape)]
1700                                                : [for(s=shape) path3d(s)]
1701                   )
1702                   are_regions_equal(apply(transform_list[0], rshape),
1703                                     apply(transform_list[L], rshape)),
1704    dummy = ends_match ? 0 : echo("WARNING: ***** The points do not match when closing the model in path_sweep() *****")
1705  )
1706  transforms ? transform_list
1707             : sweep(is_path(shape)?clockwise_polygon(shape):shape, transform_list, closed=false, caps=fullcaps,style=style,
1708                       anchor=anchor,cp=cp,spin=spin,orient=orient,atype=atype);
1709
1710
1711// Function&Module: path_sweep2d()
1712// Usage: as module
1713//   path_sweep2d(shape, path, [closed], [caps], [quality], [style], [convexity=], [anchor=], [spin=], [orient=], [atype=], [cp=]) [ATTACHMENTS];
1714// Usage: as function
1715//   vnf = path_sweep2d(shape, path, [closed], [caps], [quality], [style], [anchor=], [spin=], [orient=], [atype=], [cp=]);
1716// Description:
1717//   Takes an input 2D polygon (the shape) and a 2d path, and constructs a polyhedron by sweeping the shape along the path.
1718//   When run as a module returns the polyhedron geometry.  When run as a function returns a VNF.
1719//   .
1720//   See {{path_sweep()}} for more details on how the sweep operation works and for introductory examples.
1721//   This 2d version is different because local self-intersections (creases in the output) are allowed and do not produce CGAL errors.
1722//   This is accomplished by using offset() calculations, which are more expensive than simply copying the shape along
1723//   the path, so if you do not have local self-intersections, use {{path_sweep()}} instead.  If xmax is the largest x value (in absolute value)
1724//   of the shape, then path_sweep2d() will work as long as the offset of `path` exists at `delta=xmax`.  If the offset vanishes, as in the
1725//   case of a circle offset by more than its radius, then you will get an error about a degenerate offset.
1726//   Note that global self-intersections will still give rise to CGAL errors.  You should be able to handle these by partitioning your model.  The y axis of the
1727//   shape is mapped to the z axis in the swept polyhedron, and no twisting can occur.
1728//   The quality parameter is passed to offset to determine the offset quality.
1729// Arguments:
1730//   shape = a 2D polygon describing the shape to be swept
1731//   path = a 2D path giving the path to sweep over
1732//   closed = path is a closed loop.  Default: false
1733//   caps = true to create endcap faces when closed is false.  Can be a length 2 boolean array.  Default is true if closed is false.
1734//   quality = quality of offset used in calculation.  Default: 1
1735//   style = vnf_vertex_array style.  Default: "min_edge"
1736//   ---
1737//   convexity = convexity parameter for polyhedron (module only)  Default: 10
1738//   anchor = Translate so anchor point is at the origin.  Default: "origin"
1739//   spin = Rotate this many degrees around Z axis after anchor.  Default: 0
1740//   orient = Vector to rotate top towards after spin
1741//   atype = Select "hull" or "intersect" anchor types.  Default: "hull"
1742//   cp = Centerpoint for determining "intersect" anchors or centering the shape.  Determintes the base of the anchor vector.  Can be "centroid", "mean", "box" or a 3D point.  Default: "centroid"
1743// Anchor Types:
1744//   "hull" = Anchors to the virtual convex hull of the shape.
1745//   "intersect" = Anchors to the surface of the shape.
1746// Example: Sine wave example with self-intersections at each peak.  This would fail with path_sweep().
1747//   sinewave = [for(i=[-30:10:360*2+30]) [i/40,3*sin(i)]];
1748//   path_sweep2d(circle(r=3,$fn=15), sinewave);
1749// Example: The ends can look weird if they are in a place where self intersection occurs.  This is a natural result of how offset behaves at ends of a path.
1750//   coswave = [for(i=[0:10:360*1.5]) [i/40,3*cos(i)]];
1751//   zrot(-20)
1752//     path_sweep2d( circle(r=3,$fn=15), coswave);
1753// Example: This closed path example works ok as long as the hole in the center remains open.
1754//   ellipse = yscale(3,p=circle(r=3,$fn=120));
1755//   path_sweep2d(circle(r=2.5,$fn=32), reverse(ellipse), closed=true);
1756// Example: When the hole is closed a global intersection renders the model invalid.  You can fix this by taking the union of the two (valid) halves.
1757//   ellipse = yscale(3,p=circle(r=3,$fn=120));
1758//   L = len(ellipse);
1759//   path_sweep2d(circle(r=3.25, $fn=32), select(ellipse,floor(L*.2),ceil(L*.8)),closed=false);
1760//   path_sweep2d(circle(r=3.25, $fn=32), select(ellipse,floor(L*.7),ceil(L*.3)),closed=false);
1761
1762function path_sweep2d(shape, path, closed=false, caps, quality=1, style="min_edge",
1763                      anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull") =
1764   let(
1765        caps = is_def(caps) ? caps
1766             : closed ? false : true,
1767        capsOK = is_bool(caps) || is_bool_list(caps,2),
1768        fullcaps = is_bool(caps) ? [caps,caps] : caps,
1769        shape = force_path(shape,"shape"),
1770        path = force_path(path)
1771   )
1772   assert(is_path(shape,2), "shape must be a 2D path")
1773   assert(is_path(path,2), "path must be a 2D path")
1774   assert(capsOK, "caps must be boolean or a list of two booleans")
1775   assert(!closed || !caps, "Cannot make closed shape with caps")
1776   let(
1777        profile = ccw_polygon(shape),
1778        flip = closed && is_polygon_clockwise(path) ? -1 : 1,
1779        path = flip ? reverse(path) : path,
1780        proflist= transpose(
1781                     [for(pt = profile)
1782                        let(
1783                            ofs = offset(path, delta=-flip*pt.x, return_faces=true,closed=closed, quality=quality),
1784                            map = column(_ofs_vmap(ofs,closed=closed),1)
1785                        )
1786                        select(path3d(ofs[0],pt.y),map)
1787                      ]
1788                  ),
1789        vnf = vnf_vertex_array([
1790                         each proflist,
1791                         if (closed) proflist[0]
1792                        ],cap1=fullcaps[0],cap2=fullcaps[1],col_wrap=true,style=style)
1793   )
1794   reorient(anchor,spin,orient,vnf=vnf,p=vnf,extent=atype=="hull",cp=cp);
1795
1796
1797module path_sweep2d(profile, path, closed=false, caps, quality=1, style="min_edge", convexity=10,
1798                    anchor="origin", cp="centroid", spin=0, orient=UP, atype="hull")
1799{
1800   vnf = path_sweep2d(profile, path, closed, caps, quality, style);
1801   vnf_polyhedron(vnf,convexity=convexity,anchor=anchor, spin=spin, orient=orient, atype=atype, cp=cp)
1802        children();
1803}
1804
1805// Extract vertex mapping from offset face list.  The output of this function
1806// is a list of pairs [i,j] where i is an index into the parent curve and j is
1807// an index into the offset curve.  It would probably make sense to rewrite
1808// offset() to return this instead of the face list and have offset_sweep
1809// use this input to assemble the faces it needs.
1810
1811function _ofs_vmap(ofs,closed=false) =
1812    let(   // Caclulate length of the first (parent) curve
1813        firstlen = max(flatten(ofs[1]))+1-len(ofs[0])
1814    )
1815    [
1816     for(entry=ofs[1]) _ofs_face_edge(entry,firstlen),
1817     if (!closed) _ofs_face_edge(last(ofs[1]),firstlen,second=true)
1818    ];
1819
1820
1821// Extract first (default) or second edge that connects the parent curve to its offset.  The first input
1822// face is a list of 3 or 4 vertices as indices into the two curves where the parent curve vertices are
1823// numbered from 0 to firstlen-1 and the offset from firstlen and up.  The firstlen pararameter is used
1824// to determine which curve the vertices belong to and to remove the offset so that the return gives
1825// the index into each curve with a 0 base.
1826function _ofs_face_edge(face,firstlen,second=false) =
1827   let(
1828       itry = min_index(face),
1829       i = select(face,itry-1)<firstlen ? itry-1:itry,
1830       edge1 = select(face,[i,i-1]),
1831       edge2 = select(face,i+1)<firstlen ? select(face,[i+1,i+2])
1832                                         : select(face,[i,i+1])
1833   )
1834   (second ? edge2 : edge1)-[0,firstlen];
1835
1836
1837
1838// Function&Module: sweep()
1839// Usage: As Module
1840//   sweep(shape, transforms, [closed], [caps], [style], [convexity=], [anchor=], [spin=], [orient=], [atype=]) [ATTACHMENTS];
1841// Usage: As Function
1842//   vnf = sweep(shape, transforms, [closed], [caps], [style], [anchor=], [spin=], [orient=], [atype=]);
1843// Description:
1844//   The input `shape` must be a non-self-intersecting 2D polygon or region, and `transforms`
1845//   is a list of 4x4 transformation matrices.  The sweep algorithm applies each transformation in sequence
1846//   to the shape input and links the resulting polygons together to form a polyhedron.
1847//   If `closed=true` then the first and last transformation are linked together.
1848//   The `caps` parameter controls whether the ends of the shape are closed.
1849//   As a function, returns the VNF for the polyhedron.  As a module, computes the polyhedron.
1850//   .
1851//   Note that this is a very powerful, general framework for producing polyhedra.  It is important
1852//   to ensure that your resulting polyhedron does not include any self-intersections, or it will
1853//   be invalid and will generate CGAL errors.  If you get such errors, most likely you have an
1854//   overlooked self-intersection.  Note also that the errors will not occur when your shape is alone
1855//   in your model, but will arise if you add a second object to the model.  This may mislead you into
1856//   thinking the second object caused a problem.  Even adding a simple cube to the model will reveal the problem.
1857// Arguments:
1858//   shape = 2d path or region, describing the shape to be swept.
1859//   transforms = list of 4x4 matrices to apply
1860//   closed = set to true to form a closed (torus) model.  Default: false
1861//   caps = true to create endcap faces when closed is false.  Can be a singe boolean to specify endcaps at both ends, or a length 2 boolean array.  Default is true if closed is false.
1862//   style = vnf_vertex_array style.  Default: "min_edge"
1863//   ---
1864//   convexity = convexity setting for use with polyhedron.  (module only) Default: 10
1865//   cp = Centerpoint for determining "intersect" anchors or centering the shape.  Determintes the base of the anchor vector.  Can be "centroid", "mean", "box" or a 3D point.  Default: "centroid"
1866//   atype = Select "hull" or "intersect" anchor types.  Default: "hull"
1867//   anchor = Translate so anchor point is at the origin. Default: "origin"
1868//   spin = Rotate this many degrees around Z axis after anchor. Default: 0
1869//   orient = Vector to rotate top towards after spin  (module only)
1870// Anchor Types:
1871//   "hull" = Anchors to the virtual convex hull of the shape.
1872//   "intersect" = Anchors to the surface of the shape.
1873// Example(VPR=[45,0,74],VPD=175,VPT=[-3.8,12.4,19]): A bent object that also changes shape along its length.
1874//   radius = 75;
1875//   angle = 40;
1876//   shape = circle(r=5,$fn=32);
1877//   T = [for(i=[0:25]) xrot(-angle*i/25,cp=[0,radius,0])*scale([1+i/25, 2-i/25,1])];
1878//   sweep(shape,T);
1879// Example: This is the "sweep-drop" example from list-comprehension-demos.
1880//   function drop(t) = 100 * 0.5 * (1 - cos(180 * t)) * sin(180 * t) + 1;
1881//   function path(t) = [0, 0, 80 + 80 * cos(180 * t)];
1882//   function rotate(t) = 180 * pow((1 - t), 3);
1883//   step = 0.01;
1884//   path_transforms = [for (t=[0:step:1-step]) translate(path(t)) * zrot(rotate(t)) * scale([drop(t), drop(t), 1])];
1885//   sweep(circle(1, $fn=12), path_transforms);
1886// Example: Another example from list-comprehension-demos
1887//   function f(x) = 3 - 2.5 * x;
1888//   function r(x) = 2 * 180 * x * x * x;
1889//   pathstep = 1;
1890//   height = 100;
1891//   shape_points = subdivide_path(square(10),40,closed=true);
1892//   path_transforms = [for (i=[0:pathstep:height]) let(t=i/height) up(i) * scale([f(t),f(t),i]) * zrot(r(t))];
1893//   sweep(shape_points, path_transforms);
1894// Example: Twisted container.  Note that this technique doesn't create a fixed container wall thickness.
1895//   shape = subdivide_path(square(30,center=true), 40, closed=true);
1896//   outside = [for(i=[0:24]) up(i)*rot(i)*scale(1.25*i/24+1)];
1897//   inside = [for(i=[24:-1:2]) up(i)*rot(i)*scale(1.2*i/24+1)];
1898//   sweep(shape, concat(outside,inside));
1899
1900function sweep(shape, transforms, closed=false, caps, style="min_edge",
1901               anchor="origin", cp="centroid", spin=0, orient=UP, atype="hull") =
1902    assert(is_consistent(transforms, ident(4)), "Input transforms must be a list of numeric 4x4 matrices in sweep")
1903    assert(is_path(shape,2) || is_region(shape), "Input shape must be a 2d path or a region.")
1904    let(
1905        caps = is_def(caps) ? caps :
1906            closed ? false : true,
1907        capsOK = is_bool(caps) || is_bool_list(caps,2),
1908        fullcaps = is_bool(caps) ? [caps,caps] : caps
1909    )
1910    assert(len(transforms), "transformation must be length 2 or more")
1911    assert(capsOK, "caps must be boolean or a list of two booleans")
1912    assert(!closed || !caps, "Cannot make closed shape with caps")
1913    is_region(shape)? let(
1914        regions = region_parts(shape),
1915        rtrans = reverse(transforms),
1916        vnfs = [
1917            for (rgn=regions) each [
1918                for (path=rgn)
1919                    sweep(path, transforms, closed=closed, caps=false),
1920                if (fullcaps[0]) vnf_from_region(rgn, transform=transforms[0], reverse=true),
1921                if (fullcaps[1]) vnf_from_region(rgn, transform=last(transforms)),
1922            ],
1923        ],
1924        vnf = vnf_join(vnfs)
1925    ) vnf :
1926    assert(len(shape)>=3, "shape must be a path of at least 3 non-colinear points")
1927    vnf_vertex_array([for(i=[0:len(transforms)-(closed?0:1)]) apply(transforms[i%len(transforms)],path3d(shape))],
1928                     cap1=fullcaps[0],cap2=fullcaps[1],col_wrap=true,style=style);
1929
1930
1931module sweep(shape, transforms, closed=false, caps, style="min_edge", convexity=10,
1932             anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull")
1933{
1934    vnf = sweep(shape, transforms, closed, caps, style);
1935    vnf_polyhedron(vnf, convexity=convexity, anchor=anchor, spin=spin, orient=orient, atype=atype, cp=cp)
1936        children();
1937}
1938
1939
1940
1941// Section: Functions for resampling and slicing profile lists
1942
1943// Function: subdivide_and_slice()
1944// Topics: Paths, Path Subdivision
1945// Usage:
1946//   newprof = subdivide_and_slice(profiles, slices, [numpoints], [method], [closed]);
1947// Description:
1948//   Subdivides the input profiles to have length `numpoints` where `numpoints` must be at least as
1949//   big as the largest input profile.  By default `numpoints` is set equal to the length of the
1950//   largest profile.  You can set `numpoints="lcm"` to sample to the least common multiple of all
1951//   curves, which will avoid sampling artifacts but may produce a huge output.  After subdivision,
1952//   profiles are sliced.
1953// Arguments:
1954//   profiles = profiles to operate on
1955//   slices = number of slices to insert between each pair of profiles.  May be a vector
1956//   numpoints = number of points after sampling.
1957//   method = method used for calling {{subdivide_path()}}, either `"length"` or `"segment"`.  Default: `"length"`
1958//   closed = the first and last profile are connected.  Default: false
1959function subdivide_and_slice(profiles, slices, numpoints, method="length", closed=false) =
1960  let(
1961    maxsize = max_length(profiles),
1962    numpoints = is_undef(numpoints) ? maxsize :
1963                numpoints == "lcm" ? lcmlist([for(p=profiles) len(p)]) :
1964                is_num(numpoints) ? round(numpoints) : undef
1965  )
1966  assert(is_def(numpoints), "Parameter numpoints must be \"max\", \"lcm\" or a positive number")
1967  assert(numpoints>=maxsize, "Number of points requested is smaller than largest profile")
1968  let(fixpoly = [for(poly=profiles) subdivide_path(poly, numpoints,method=method)])
1969  slice_profiles(fixpoly, slices, closed);
1970
1971
1972
1973// Function: slice_profiles()
1974// Topics: Paths, Path Subdivision
1975// Usage:
1976//   profs = slice_profiles(profiles, slices, [closed]);
1977// Description:
1978//   Given an input list of profiles, linearly interpolate between each pair to produce a
1979//   more finely sampled list.  The parameters `slices` specifies the number of slices to
1980//   be inserted between each pair of profiles and can be a number or a list.
1981// Arguments:
1982//   profiles = list of paths to operate on.  They must be lists of the same shape and length.
1983//   slices = number of slices to insert between each pair, or a list to vary the number inserted.
1984//   closed = set to true if last profile connects to first one.  Default: false
1985function slice_profiles(profiles,slices,closed=false) =
1986  assert(is_num(slices) || is_list(slices))
1987  let(listok = !is_list(slices) || len(slices)==len(profiles)-(closed?0:1))
1988  assert(listok, "Input slices to slice_profiles is a list with the wrong length")
1989  let(
1990    count = is_num(slices) ? repeat(slices,len(profiles)-(closed?0:1)) : slices,
1991    slicelist = [for (i=[0:len(profiles)-(closed?1:2)])
1992      each lerpn(profiles[i], select(profiles,i+1), count[i]+1, false)
1993    ]
1994  )
1995  concat(slicelist, closed?[]:[profiles[len(profiles)-1]]);
1996
1997
1998
1999function _closest_angle(alpha,beta) =
2000    is_vector(beta) ? [for(entry=beta) _closest_angle(alpha,entry)]
2001  : beta-alpha > 180 ? beta - ceil((beta-alpha-180)/360) * 360
2002  : beta-alpha < -180 ? beta + ceil((alpha-beta-180)/360) * 360
2003  : beta;
2004
2005
2006// Smooth data with N point moving average.  If angle=true handles data as angles.
2007// If closed=true assumes last point is adjacent to the first one.
2008// If closed=false pads data with left/right value (probably wrong behavior...should do linear interp)
2009function _smooth(data,len,closed=false,angle=false) =
2010  let(  halfwidth = floor(len/2),
2011        result = closed ? [for(i=idx(data))
2012                           let(
2013                             window = angle ? _closest_angle(data[i],select(data,i-halfwidth,i+halfwidth))
2014                                            : select(data,i-halfwidth,i+halfwidth)
2015                           )
2016                           mean(window)]
2017               : [for(i=idx(data))
2018                   let(
2019                       window = select(data,max(i-halfwidth,0),min(i+halfwidth,len(data)-1)),
2020                       left = i-halfwidth<0,
2021                       pad = left ? data[0] : last(data)
2022                   )
2023                   sum(window)+pad*(len-len(window))] / len
2024   )
2025   result;
2026
2027
2028// Function: rot_resample()
2029// Usage:
2030//   rlist = rot_resample(rotlist, n, [method=], [twist=], [scale=], [smoothlen=], [long=], [turns=], [closed=])
2031// Description:
2032//   Takes as input a list of rotation matrices in 3d.  Produces as output a resampled
2033//   list of rotation operators (4x4 matrixes) suitable for use with sweep().  You can optionally apply twist to
2034//   the output with the twist parameter, which is either a scalar to apply a uniform
2035//   overall twist, or a vector to apply twist non-uniformly.  Similarly you can apply
2036//   scaling either overall or with a vector.  The smoothlen parameter applies smoothing
2037//   to the twist and scaling to prevent abrupt changes.  This is done by a moving average
2038//   of the smoothing or scaling values.  The default of 1 means no smoothing.  The long parameter causes
2039//   the interpolation to be done the "long" way around the rotation instead of the short way.
2040//   Note that the rotation matrix cannot distinguish which way you rotate, only the place you
2041//   end after rotation.  Another ambiguity arises if your rotation is more than 360 degrees.
2042//   You can add turns with the turns parameter, so giving turns=1 will add 360 degrees to the
2043//   rotation so it completes one full turn plus the additional rotation given my the transform.
2044//   You can give long as a scalar or as a vector.  Finally if closed is true then the
2045//   resampling will connect back to the beginning.
2046//   .
2047//   The default is to resample based on the length of the arc defined by each rotation operator.  This produces
2048//   uniform sampling over all of the transformations.  It requires that each rotation has nonzero length.
2049//   In this case n specifies the total number of samples.  If you set method to "count" then you get
2050//   n samples for each transform.  You can set n to a vector to vary the samples at each step.
2051// Arguments:
2052//   rotlist = list of rotation operators in 3d to resample
2053//   n = Number of rotations to produce as output when method is "length" or number for each transformation if method is "count".  Can be a vector when method is "count"
2054//   ---
2055//   method = sampling method, either "length" or "count"
2056//   twist = scalar or vector giving twist to add overall or at each rotation.  Default: none
2057//   scale = scalar or vector giving scale factor to add overall or at each rotation.  Default: none
2058//   smoothlen = amount of smoothing to apply to scaling and twist.  Should be an odd integer.  Default: 1
2059//   long = resample the "long way" around the rotation, a boolean or list of booleans.  Default: false
2060//   turns = add extra turns.  If a scalar adds the turns to every rotation, or give a vector.  Default: 0
2061//   closed = if true then the rotation list is treated as closed.  Default: false
2062// Example(3D): Resampling the arc from a compound rotation with translations thrown in.
2063//   tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], n=25);
2064//   sweep(circle(r=1,$fn=3), tran);
2065// Example(3D): Applying a scale factor
2066//   tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], n=25, scale=2);
2067//   sweep(circle(r=1,$fn=3), tran);
2068// Example(3D): Applying twist
2069//   tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], n=25, twist=60);
2070//   sweep(circle(r=1,$fn=3), tran);
2071// Example(3D): Going the long way
2072//   tran = rot_resample([ident(4), back(5)*up(4)*xrot(-10)*zrot(-20)*yrot(117,cp=[10,0,0])], n=25, long=true);
2073//   sweep(circle(r=1,$fn=3), tran);
2074// Example(3D): Getting transformations from turtle3d
2075//   include<BOSL2/turtle3d.scad>
2076//   tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,170],transforms=true);
2077//   sweep(circle(r=1,$fn=3),rot_resample(tran, n=40));
2078// Example(3D): If you specify a larger angle in turtle you need to use the long argument
2079//   include<BOSL2/turtle3d.scad>
2080//   tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,270],transforms=true);
2081//   sweep(circle(r=1,$fn=3),rot_resample(tran, n=40,long=true));
2082// Example(3D): And if the angle is over 360 you need to add turns to get the right result.  Note long is false when the remaining angle after subtracting full turns is below 180:
2083//   include<BOSL2/turtle3d.scad>
2084//   tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,90+360],transforms=true);
2085//   sweep(circle(r=1,$fn=3),rot_resample(tran, n=40,long=false,turns=1));
2086// Example(3D): Here the remaining angle is 270, so long must be set to true
2087//   include<BOSL2/turtle3d.scad>
2088//   tran=turtle3d(["arcsteps",1,"up", 10, "arczrot", 10,270+360],transforms=true);
2089//   sweep(circle(r=1,$fn=3),rot_resample(tran, n=40,long=true,turns=1));
2090// Example(3D): Note the visible line at the scale transition
2091//   include<BOSL2/turtle3d.scad>
2092//   tran = turtle3d(["arcsteps",1,"arcup", 10, 90, "arcdown", 10, 90], transforms=true);
2093//   rtran = rot_resample(tran,200,scale=[1,6]);
2094//   sweep(circle(1,$fn=32),rtran);
2095// Example(3D): Observe how using a large smoothlen value eases that transition
2096//   include<BOSL2/turtle3d.scad>
2097//   tran = turtle3d(["arcsteps",1,"arcup", 10, 90, "arcdown", 10, 90], transforms=true);
2098//   rtran = rot_resample(tran,200,scale=[1,6],smoothlen=17);
2099//   sweep(circle(1,$fn=32),rtran);
2100// Example(3D): A similar issues can arise with twist, where a "line" is visible at the transition
2101//   include<BOSL2/turtle3d.scad>
2102//   tran = turtle3d(["arcsteps", 1, "arcup", 10, 90, "move", 10], transforms=true,state=[1,-.5,0]);
2103//   rtran = rot_resample(tran,100,twist=[0,60],smoothlen=1);
2104//   sweep(subdivide_path(rect([3,3]),40),rtran);
2105// Example(3D): Here's the smoothed twist transition
2106//   include<BOSL2/turtle3d.scad>
2107//   tran = turtle3d(["arcsteps", 1, "arcup", 10, 90, "move", 10], transforms=true,state=[1,-.5,0]);
2108//   rtran = rot_resample(tran,100,twist=[0,60],smoothlen=17);
2109//   sweep(subdivide_path(rect([3,3]),40),rtran);
2110// Example(3D): Toothed belt based on a list-comprehension-demos example.  This version has a smoothed twist transition.  Try changing smoothlen to 1 to see the more abrupt transition that occurs without smoothing.
2111//   include<BOSL2/turtle3d.scad>
2112//   r_small = 19;       // radius of small curve
2113//   r_large = 46;       // radius of large curve
2114//   flat_length = 100;  // length of flat belt section
2115//   teeth=42;           // number of teeth
2116//   belt_width = 12;
2117//   tooth_height = 9;
2118//   belt_thickness = 3;
2119//   angle = 180 - 2*atan((r_large-r_small)/flat_length);
2120//   beltprofile = path3d(subdivide_path(
2121//                   square([belt_width, belt_thickness],anchor=FWD),
2122//                   20));
2123//   beltrots =
2124//     turtle3d(["arcsteps",1,
2125//               "move", flat_length,
2126//               "arcleft", r_small, angle,
2127//               "move", flat_length,
2128//     // Closing path will be interpolated
2129//     //        "arcleft", r_large, 360-angle
2130//              ],transforms=true);
2131//   beltpath = rot_resample(beltrots,teeth*4,
2132//                           twist=[180,0,-180,0],
2133//                           long=[false,false,false,true],
2134//                           smoothlen=15,closed=true);
2135//   belt = [for(i=idx(beltpath))
2136//             let(tooth = floor((i+$t*4)/2)%2)
2137//             apply(beltpath[i]*
2138//                     yscale(tooth
2139//                            ? tooth_height/belt_thickness
2140//                            : 1),
2141//                   beltprofile)
2142//          ];
2143//   skin(belt,slices=0,closed=true);
2144function rot_resample(rotlist,n,twist,scale,smoothlen=1,long=false,turns=0,closed=false,method="length") =
2145    assert(is_int(smoothlen) && smoothlen>0 && smoothlen%2==1, "smoothlen must be a positive odd integer")
2146    assert(method=="length" || method=="count")
2147    let(tcount = len(rotlist) + (closed?0:-1))
2148    assert(method=="count" || is_int(n), "n must be an integer when method is \"length\"")
2149    assert(is_int(n) || is_vector(n,tcount), str("n must be scalar or vector with length ",tcount))
2150    let(
2151          count = method=="length" ? (closed ? n+1 : n)
2152                                   : (is_vector(n) ? sum(n) : tcount*n)+1  //(closed?0:1)
2153    )
2154    assert(is_bool(long) || len(long)==tcount,str("Input long must be a scalar or have length ",tcount))
2155    let(
2156        long = force_list(long,tcount),
2157        turns = force_list(turns,tcount),
2158        T = [for(i=[0:1:tcount-1]) rot_inverse(rotlist[i])*select(rotlist,i+1)],
2159        parms = [for(i=idx(T))
2160                    let(tparm = rot_decode(T[i],long[i]))
2161                    [tparm[0]+turns[i]*360,tparm[1],tparm[2],tparm[3]]
2162                ],
2163        radius = [for(i=idx(parms)) norm(parms[i][2])],
2164        length = [for(i=idx(parms)) norm([norm(parms[i][3]), parms[i][0]/360*2*PI*radius[i]])]
2165    )
2166    assert(method=="count" || all_positive(length),
2167           "Rotation list includes a repeated entry or a rotation around the origin, not allowed when method=\"length\"")
2168    let(
2169        cumlen = [0, each cumsum(length)],
2170        totlen = last(cumlen),
2171        stepsize = totlen/(count-1),
2172        samples = method=="count"
2173                  ? let( n = force_list(n,tcount))
2174                    [for(N=n) lerpn(0,1,N,endpoint=false)]
2175                  :[for(i=idx(parms))
2176                    let(
2177                        remainder = cumlen[i] % stepsize,
2178                        offset = remainder==0 ? 0
2179                                              : stepsize-remainder,
2180                        num = ceil((length[i]-offset)/stepsize)
2181                    )
2182                    count(num,offset,stepsize)/length[i]],
2183         twist = first_defined([twist,0]),
2184         scale = first_defined([scale,1]),
2185         needlast = !approx(last(last(samples)),1),
2186         sampletwist = is_num(twist) ? lerpn(0,twist,count)
2187                     : let(
2188                          cumtwist = [0,each cumsum(twist)]
2189                      )
2190                      [for(i=idx(parms)) each lerp(cumtwist[i],cumtwist[i+1],samples[i]),
2191                      if (needlast) last(cumtwist)
2192                      ],
2193         samplescale = is_num(scale) ? lerp(1,scale,lerpn(0,1,count))
2194                     : let(
2195                          cumscale = [1,each cumprod(scale)]
2196                      )
2197                      [for(i=idx(parms)) each lerp(cumscale[i],cumscale[i+1],samples[i]),
2198                       if (needlast) last(cumscale)],
2199         smoothtwist = _smooth(closed?select(sampletwist,0,-2):sampletwist,smoothlen,closed=closed,angle=true),
2200         smoothscale = _smooth(samplescale,smoothlen,closed=closed),
2201         interpolated = [
2202           for(i=idx(parms))
2203             each [for(u=samples[i]) rotlist[i] * move(u*parms[i][3]) * rot(a=u*parms[i][0],v=parms[i][1],cp=parms[i][2])],
2204           if (needlast) last(rotlist)
2205         ]
2206     )
2207     [for(i=idx(interpolated,e=closed?-2:-1)) interpolated[i]*zrot(smoothtwist[i])*scale(smoothscale[i])];
2208
2209
2210
2211
2212
2213//////////////////////////////////////////////////////////////////
2214//
2215// Minimum Distance Mapping using Dynamic Programming
2216//
2217// Given inputs of a two polygons, computes a mapping between their vertices that minimizes the sum the sum of
2218// the distances between every matched pair of vertices.  The algorithm uses dynamic programming to calculate
2219// the optimal mapping under the assumption that poly1[0] <-> poly2[0].  We then rotate through all the
2220// possible indexings of the longer polygon.  The theoretical run time is quadratic in the longer polygon and
2221// linear in the shorter one.
2222//
2223// The top level function, _skin_distance_match(), cycles through all the of the indexings of the larger
2224// polygon, computes the optimal value for each indexing, and chooses the overall best result.  It uses
2225// _dp_extract_map() to thread back through the dynamic programming array to determine the actual mapping, and
2226// then converts the result to an index repetition count list, which is passed to repeat_entries().
2227//
2228// The function _dp_distance_array builds up the rows of the dynamic programming matrix with reference
2229// to the previous rows, where `tdist` holds the total distance for a given mapping, and `map`
2230// holds the information about which path was optimal for each position.
2231//
2232// The function _dp_distance_row constructs each row of the dynamic programming matrix in the usual
2233// way where entries fill in based on the three entries above and to the left.  Note that we duplicate
2234// entry zero so account for wrap-around at the ends, and we initialize the distance to zero to avoid
2235// double counting the length of the 0-0 pair.
2236//
2237// This function builds up the dynamic programming distance array where each entry in the
2238// array gives the optimal distance for aligning the corresponding subparts of the two inputs.
2239// When the array is fully populated, the bottom right corner gives the minimum distance
2240// for matching the full input lists.  The `map` array contains a the three key values for the three
2241// directions, where _MAP_DIAG means you map the next vertex of `big` to the next vertex of `small`,
2242// _MAP_LEFT means you map the next vertex of `big` to the current vertex of `small`, and _MAP_UP
2243// means you map the next vertex of `small` to the current vertex of `big`.
2244//
2245// Return value is [min_distance, map], where map is the array that is used to extract the actual
2246// vertex map.
2247
2248_MAP_DIAG = 0;
2249_MAP_LEFT = 1;
2250_MAP_UP = 2;
2251
2252/*
2253function _dp_distance_array(small, big, abort_thresh=1/0, small_ind=0, tdist=[], map=[]) =
2254   small_ind == len(small)+1 ? [tdist[len(tdist)-1][len(big)-1], map] :
2255   let( newrow = _dp_distance_row(small, big, small_ind, tdist) )
2256   min(newrow[0]) > abort_thresh ? [tdist[len(tdist)-1][len(big)-1],map] :
2257   _dp_distance_array(small, big, abort_thresh, small_ind+1, concat(tdist, [newrow[0]]), concat(map, [newrow[1]]));
2258*/
2259
2260
2261function _dp_distance_array(small, big, abort_thresh=1/0) =
2262   [for(
2263        small_ind = 0,
2264        tdist = [],
2265        map = []
2266           ;
2267        small_ind<=len(small)+1
2268           ;
2269        newrow =small_ind==len(small)+1 ? [0,0,0] :  // dummy end case
2270                           _dp_distance_row(small,big,small_ind,tdist),
2271        tdist = concat(tdist, [newrow[0]]),
2272        map = concat(map, [newrow[1]]),
2273        small_ind = min(newrow[0])>abort_thresh ? len(small)+1 : small_ind+1
2274       )
2275     if (small_ind==len(small)+1) each [tdist[len(tdist)-1][len(big)], map]];
2276                                     //[tdist,map]];
2277
2278
2279function _dp_distance_row(small, big, small_ind, tdist) =
2280                    // Top left corner is zero because it gets counted at the end in bottom right corner
2281   small_ind == 0 ? [cumsum([0,for(i=[1:len(big)]) norm(big[i%len(big)]-small[0])]), repeat(_MAP_LEFT,len(big)+1)] :
2282   [for(big_ind=1,
2283       newrow=[ norm(big[0] - small[small_ind%len(small)]) + tdist[small_ind-1][0] ],
2284       newmap = [_MAP_UP]
2285         ;
2286       big_ind<=len(big)+1
2287         ;
2288       costs = big_ind == len(big)+1 ? [0] :    // handle extra iteration
2289                             [tdist[small_ind-1][big_ind-1],  // diag
2290                              newrow[big_ind-1],              // left
2291                              tdist[small_ind-1][big_ind]],   // up
2292       newrow = concat(newrow, [min(costs)+norm(big[big_ind%len(big)]-small[small_ind%len(small)])]),
2293       newmap = concat(newmap, [min_index(costs)]),
2294       big_ind = big_ind+1
2295   ) if (big_ind==len(big)+1) each [newrow,newmap]];
2296
2297
2298function _dp_extract_map(map) =
2299      [for(
2300           i=len(map)-1,
2301           j=len(map[0])-1,
2302           smallmap=[],
2303           bigmap = []
2304              ;
2305           j >= 0
2306              ;
2307           advance_i = map[i][j]==_MAP_UP || map[i][j]==_MAP_DIAG,
2308           advance_j = map[i][j]==_MAP_LEFT || map[i][j]==_MAP_DIAG,
2309           i = i - (advance_i ? 1 : 0),
2310           j = j - (advance_j ? 1 : 0),
2311           bigmap = concat( [j%(len(map[0])-1)] ,  bigmap),
2312           smallmap = concat( [i%(len(map)-1)]  , smallmap)
2313          )
2314        if (i==0 && j==0) each [smallmap,bigmap]];
2315
2316
2317/// Internal Function: _skin_distance_match(poly1,poly2)
2318/// Usage:
2319///   polys = _skin_distance_match(poly1,poly2);
2320/// Description:
2321///   Find a way of associating the vertices of poly1 and vertices of poly2
2322///   that minimizes the sum of the length of the edges that connect the two polygons.
2323///   Polygons can be in 2d or 3d.  The algorithm has cubic run time, so it can be
2324///   slow if you pass large polygons.  The output is a pair of polygons with vertices
2325///   duplicated as appropriate to be used as input to `skin()`.
2326/// Arguments:
2327///   poly1 = first polygon to match
2328///   poly2 = second polygon to match
2329function _skin_distance_match(poly1,poly2) =
2330   let(
2331      swap = len(poly1)>len(poly2),
2332      big = swap ? poly1 : poly2,
2333      small = swap ? poly2 : poly1,
2334      map_poly = [ for(
2335              i=0,
2336              bestcost = 1/0,
2337              bestmap = -1,
2338              bestpoly = -1
2339              ;
2340              i<=len(big)
2341              ;
2342              shifted = list_rotate(big,i),
2343              result =_dp_distance_array(small, shifted, abort_thresh = bestcost),
2344              bestmap = result[0]<bestcost ? result[1] : bestmap,
2345              bestpoly = result[0]<bestcost ? shifted : bestpoly,
2346              best_i = result[0]<bestcost ? i : best_i,
2347              bestcost = min(result[0], bestcost),
2348              i=i+1
2349              )
2350              if (i==len(big)) each [bestmap,bestpoly,best_i]],
2351      map = _dp_extract_map(map_poly[0]),
2352      smallmap = map[0],
2353      bigmap = map[1],
2354      // These shifts are needed to handle the case when points from both ends of one curve map to a single point on the other
2355      bigshift =  len(bigmap) - max(max_index(bigmap,all=true))-1,
2356      smallshift = len(smallmap) - max(max_index(smallmap,all=true))-1,
2357      newsmall = list_rotate(repeat_entries(small,unique_count(smallmap)[1]),smallshift),
2358      newbig = list_rotate(repeat_entries(map_poly[1],unique_count(bigmap)[1]),bigshift)
2359      )
2360      swap ? [newbig, newsmall] : [newsmall,newbig];
2361
2362
2363// This function associates vertices but with the assumption that index 0 is associated between the
2364// two inputs.  This gives only quadratic run time.  As above, output is pair of polygons with
2365// vertices duplicated as suited to use as input to skin().
2366
2367function _skin_aligned_distance_match(poly1, poly2) =
2368    let(
2369      result = _dp_distance_array(poly1, poly2, abort_thresh=1/0),
2370      map = _dp_extract_map(result[1]),
2371      shift0 = len(map[0]) - max(max_index(map[0],all=true))-1,
2372      shift1 = len(map[1]) - max(max_index(map[1],all=true))-1,
2373      new0 = list_rotate(repeat_entries(poly1,unique_count(map[0])[1]),shift0),
2374      new1 = list_rotate(repeat_entries(poly2,unique_count(map[1])[1]),shift1)
2375  )
2376  [new0,new1];
2377
2378
2379//////////////////////////////////////////////////////////////////////////////////////////////////////////////
2380/// Internal Function: _skin_tangent_match()
2381/// Usage:
2382///   x = _skin_tangent_match(poly1, poly2)
2383/// Description:
2384///   Finds a mapping of the vertices of the larger polygon onto the smaller one.  Whichever input is the
2385///   shorter path is the polygon, and the longer input is the curve.  For every edge of the polygon, the algorithm seeks a plane that contains that
2386///   edge and is tangent to the curve.  There will be more than one such point.  To choose one, the algorithm centers the polygon and curve on their centroids
2387///   and chooses the closer tangent point.  The algorithm works its way around the polygon, computing a series of tangent points and then maps all of the
2388///   points on the curve between two tangent points into one vertex of the polygon.  This algorithm can fail if the curve has too few points or if it is concave.
2389/// Arguments:
2390///   poly1 = input polygon
2391///   poly2 = input polygon
2392function _skin_tangent_match(poly1, poly2) =
2393    let(
2394        swap = len(poly1)>len(poly2),
2395        big = swap ? poly1 : poly2,
2396        small = swap ? poly2 : poly1,
2397        curve_offset = centroid(small)-centroid(big),
2398        cutpts = [for(i=[0:len(small)-1]) _find_one_tangent(big, select(small,i,i+1),curve_offset=curve_offset)],
2399        shift = last(cutpts)+1,
2400        newbig = list_rotate(big, shift),
2401        repeat_counts = [for(i=[0:len(small)-1]) posmod(cutpts[i]-select(cutpts,i-1),len(big))],
2402        newsmall = repeat_entries(small,repeat_counts)
2403    )
2404    assert(len(newsmall)==len(newbig), "Tangent alignment failed, probably because of insufficient points or a concave curve")
2405    swap ? [newbig, newsmall] : [newsmall, newbig];
2406
2407
2408function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) =
2409    let(
2410        angles = [
2411            for (i = [0:len(curve)-(closed?1:2)])
2412            let(
2413                plane = plane3pt( edge[0], edge[1], curve[i]),
2414                tangent = [curve[i], select(curve,i+1)]
2415            ) plane_line_angle(plane,tangent)
2416        ],
2417        zero_cross = [
2418            for (i = [0:len(curve)-(closed?1:2)])
2419            if (sign(angles[i]) != sign(select(angles,i+1)))
2420            i
2421        ],
2422        d = [
2423            for (i = zero_cross)
2424            point_line_distance(curve[i]+curve_offset, edge)
2425        ]
2426    ) zero_cross[min_index(d)];
2427
2428
2429// Function: associate_vertices()
2430// Usage:
2431//   newpoly = associate_vertices(polygons, split);
2432// Description:
2433//   Takes as input a list of polygons and duplicates specified vertices in each polygon in the list through the series so
2434//   that the input can be passed to `skin()`.  This allows you to decide how the vertices are linked up rather than accepting
2435//   the automatically computed minimal distance linkage.  However, the number of vertices in the polygons must not decrease in the list.
2436//   The output is a list of polygons that all have the same number of vertices with some duplicates.  You specify the vertex splitting
2437//   using the `split` which is a list where each entry corresponds to a polygon: split[i] is a value or list specifying which vertices in polygon i to split.
2438//   Give the empty list if you don't want a split for a particular polygon.  If you list a vertex once then it will be split and mapped to
2439//   two vertices in the next polygon.  If you list it N times then N copies will be created to map to N+1 vertices in the next polygon.
2440//   You must ensure that each mapping produces the correct number of vertices to exactly map onto every vertex of the next polygon.
2441//   Note that if you split (only) vertex i of a polygon that means it will map to vertices i and i+1 of the next polygon.  Vertex 0 will always
2442//   map to vertex 0 and the last vertices will always map to each other, so if you want something different than that you'll need to reindex
2443//   your polygons.
2444// Arguments:
2445//   polygons = list of polygons to split
2446//   split = list of lists of split vertices
2447// Example(FlatSpin,VPD=17,VPT=[0,0,2]):  If you skin together a square and hexagon using the optimal distance method you get two triangular faces on opposite sides:
2448//   sq = regular_ngon(4,side=2);
2449//   hex = apply(rot(15),hexagon(side=2));
2450//   skin([sq,hex], slices=10, refine=10, method="distance", z=[0,4]);
2451// Example(FlatSpin,VPD=17,VPT=[0,0,2]):  Using associate_vertices you can change the location of the triangular faces.  Here they are connect to two adjacent vertices of the square:
2452//   sq = regular_ngon(4,side=2);
2453//   hex = apply(rot(15),hexagon(side=2));
2454//   skin(associate_vertices([sq,hex],[[1,2]]), slices=10, refine=10, sampling="segment", z=[0,4]);
2455// Example(FlatSpin,VPD=17,VPT=[0,0,2]): Here the two triangular faces connect to a single vertex on the square.  Note that we had to rotate the hexagon to line them up because the vertices match counting forward, so in this case vertex 0 of the square matches to vertices 0, 1, and 2 of the hexagon.
2456//   sq = regular_ngon(4,side=2);
2457//   hex = apply(rot(60),hexagon(side=2));
2458//   skin(associate_vertices([sq,hex],[[0,0]]), slices=10, refine=10, sampling="segment", z=[0,4]);
2459// Example(3D): This example shows several polygons, with only a single vertex split at each step:
2460//   sq = regular_ngon(4,side=2);
2461//   pent = pentagon(side=2);
2462//   hex = hexagon(side=2);
2463//   sep = regular_ngon(7,side=2);
2464//   profiles = associate_vertices([sq,pent,hex,sep], [1,3,4]);
2465//   skin(profiles ,slices=10, refine=10, method="distance", z=[0,2,4,6]);
2466// Example(3D): The polygons cannot shrink, so if you want to have decreasing polygons you'll need to concatenate multiple results.  Note that it is perfectly ok to duplicate a profile as shown here, where the pentagon is duplicated:
2467//   sq = regular_ngon(4,side=2);
2468//   pent = pentagon(side=2);
2469//   grow = associate_vertices([sq,pent], [1]);
2470//   shrink = associate_vertices([sq,pent], [2]);
2471//   skin(concat(grow, reverse(shrink)), slices=10, refine=10, method="distance", z=[0,2,2,4]);
2472function associate_vertices(polygons, split, curpoly=0) =
2473   curpoly==len(polygons)-1 ? polygons :
2474   let(
2475      polylen = len(polygons[curpoly]),
2476      cursplit = force_list(split[curpoly])
2477   )
2478    assert(len(split)==len(polygons)-1,str(split,"Split list length mismatch: it has length ", len(split)," but must have length ",len(polygons)-1))
2479    assert(polylen<=len(polygons[curpoly+1]),str("Polygon ",curpoly," has more vertices than the next one."))
2480    assert(len(cursplit)+polylen == len(polygons[curpoly+1]),
2481           str("Polygon ", curpoly, " has ", polylen, " vertices.  Next polygon has ", len(polygons[curpoly+1]),
2482                  " vertices.  Split list has length ", len(cursplit), " but must have length ", len(polygons[curpoly+1])-polylen))
2483    assert(max(cursplit)<polylen && min(curpoly)>=0,
2484           str("Split ",cursplit," at polygon ",curpoly," has invalid vertices.  Must be in [0:",polylen-1,"]"))
2485    len(cursplit)==0 ? associate_vertices(polygons,split,curpoly+1) :
2486    let(
2487      splitindex = sort(concat(count(polylen), cursplit)),
2488      newpoly = [for(i=[0:len(polygons)-1]) i<=curpoly ? select(polygons[i],splitindex) : polygons[i]]
2489    )
2490   associate_vertices(newpoly, split, curpoly+1);
2491
2492
2493
2494// DefineHeader(Table;Headers=Texture Name|Type|Description): Texture Values
2495
2496// Section: Texturing
2497
2498// Function: texture()
2499// Usage:
2500//   tx = texture(tex, [n=], [inset=], [gap=], [roughness=]);
2501// Topics: Textures, Knurling
2502// Description:
2503//   Given a texture name, returns a texture.  Textures can come in two varieties:
2504//   - Heightfield textures which are 2D arrays of scalars.  These are faster to render, but are less precise and prone to triangulation errors.
2505//   - VNF Tile textures, which are VNFs that completely tile the rectangle `[0,0]` to `[1,1]`.  These tend to be slower to render, but are more precise.
2506//   Sometimes the geometry of a shape to be textured will cause a heightfield texture to be badly triangulated.
2507//   Switching to a similar VNF tile texture can solve this problem.  Usually just by adding the prefix "vnf_".
2508// Texture Values:
2509//   "bricks"       = Heightfield = A brick-wall pattern.  Giving `n=` sets the number of heightfield samples to `n` by `n`.  Giving `roughness=` adds a level of height randomization to add roughness to the texture.
2510//   "bricks_vnf"   = VNF Tile = Like "bricks", but slower and more consistent in triangulation.  Giving `gap=` sets the mortar gap between bricks.  Giving `inset=` specifies the inset of the brick tops, relative to their bottoms.
2511//   "checkers"     = VNF Tile = A pattern of alternating checkerboard squares.  Giving `inset=` specifies the inset of the raised checker tile tops, relative to the lowered tiles.
2512//   "cones"        = VNF Tile = Raised conical spikes.  Giving `n=` sets the number of sides to the cone.  Giving `inset=` specifies the inset of the base of the cone, relative to the tile edges.
2513//   "cubes"        = VNF Tile = Cornercubes texture.
2514//   "diamonds"     = Heightfield = Diamond shapes with tips aligned with the axes.  Useful for knurling.  Giving `n=` sets the number of heightfield samples to `n` by `n`.
2515//   "diamonds_vnf" = VNF Tile = Like "diamonds", but slower and more consistent in triangulation.
2516//   "dimples"      = VNF Tile = Small round divots.  Giving `n=` sets the resolution of the divot curve.  Giving `inset=` specifies the inset of the dimples, relative to the edge of the tile.
2517//   "dots"         = VNF Tile = Raised small round bumps.  Giving `n=` sets the resolution of the bump curve.  Giving `inset=` specifies the inset of the dots, relative to the edge of the tile.
2518//   "hex_grid"     = VNF Tile = A hexagonal grid of thin lines.  Giving `inset=` specifies the inset of the hex tops, relative to their bottoms.
2519//   "hills"        = Heightfield = Wavy sine-wave hills and valleys,  Giving `n=` sets the number of heightfield samples to `n` by `n`.
2520//   "pyramids"     = Heightfield = Pyramids shapes with flat sides aligned with the axes.  Also useful for knurling.  Giving `n=` sets the number of heightfield samples to `n` by `n`.
2521//   "pyramids_vnf" = VNF Tile = Like "pyramids", but slower and more consistent in triangulation.
2522//   "ribs"         = Heightfield = Vertically aligned triangular ribs.  Giving `n=` sets the number of heightfield samples to `n` by `1`.
2523//   "rough"        = Heightfield = A pseudo-randomized rough surace texture.  Giving `n=` sets the number of heightfield samples to `n` by `n`.  Giving `roughness=` adds a level of height randomization to add roughness to the texture.
2524//   "tri_grid"     = VNF Tile = A triangular grid of thin lines.  Giving `inset=` specifies the inset of the triangle tops, relative to their bottoms.
2525//   "trunc_diamonds"     = VNF Tile = Truncated diamonds.  A grid of thin lines at 45º angles.  Giving `inset=` specifies the inset of the truncated diamond tops, relative to their bottoms.
2526//   "trunc_pyramids"     = Heightfield = Truncated pyramids.  Like "pyramids" but with flattened tips.  Giving `n=` sets the number of heightfield samples to `n` by `n`.
2527//   "trunc_pyramids_vnf" = VNF Tile = Like "trunc_pyramids", but slower and more consistent in triangulation.  Giving `inset=` specifies the inset of the truncated pyramid tops, relative to their bottoms.
2528//   "trunc_ribs"         = Heightfield = Truncated ribs.  Like "ribs" but with flat rib tips.  Giving `n=` sets the number of heightfield samples to `n` by `1`.
2529//   "trunc_ribs_vnf"     = VNF Tile = Like "trunc_ribs", but slower and more adjustable.  Giving `gap=` sets the bottom gap between ribs.  Giving `inset=` specifies the inset of the rib tops, relative to their bottoms.
2530//   "wave_ribs"          = Heightfield = Vertically aligned wavy ribs.  Giving `n=` sets the number of heightfield samples to `n` by `1`.
2531// Arguments:
2532//   tex = The name of the texture to get.
2533//   ---
2534//   n = The general number of vertices to use to refine the resolution of the texture.
2535//   inset = The amount to inset part of a VNF tile texture.  Generally between 0 and 0.5.
2536//   gap = The gap between logically distinct parts of a VNF tile.  (ie: gap between bricks, gap between truncated ribs, etc.)
2537//   roughness = The amount of roughness used on the surface of some heightfield textures.  Generally between 0 and 0.5.
2538// See Also: heightfield(), cylindrical_heightfield(), texture()
2539// Example(3D): "ribs" texture.
2540//   tex = texture("ribs");
2541//   linear_sweep(
2542//       rect(50), texture=tex, h=40, tex_scale=3,
2543//       tex_size=[10,10], style="concave"
2544//   );
2545// Example(3D): Truncated "trunc_ribs" texture.
2546//   tex = texture("trunc_ribs");
2547//   linear_sweep(
2548//       rect(50), h=40, texture=tex,
2549//       tex_scale=3, tex_size=[10,10],
2550//       style="concave"
2551//   );
2552// Example(3D): "trunc_ribs_vnf" texture.  Slower, but more controllable.
2553//   tex = texture("trunc_ribs_vnf", gap=0.25, inset=0.333);
2554//   linear_sweep(
2555//       rect(50), h=40, texture=tex,
2556//       tex_scale=3, tex_size=[10,10]
2557//   );
2558// Example(3D): "wave_ribs" texture.
2559//   tex = texture("wave_ribs");
2560//   linear_sweep(
2561//       rect(50), h=40, texture=tex,
2562//       tex_size=[10,10], style="concave"
2563//   );
2564// Example(3D): "diamonds" texture.
2565//   tex = texture("diamonds");
2566//   linear_sweep(
2567//       rect(50), texture=tex, h=40,
2568//       tex_size=[10,10], style="concave"
2569//   );
2570// Example(3D): "diamonds_vnf" texture.  Slower, but more consistent around complex curves.
2571//   tex = texture("diamonds_vnf");
2572//   linear_sweep(
2573//       rect(50), texture=tex, h=40,
2574//       tex_size=[10,10]
2575//   );
2576// Example(3D): "pyramids" texture.
2577//   tex = texture("pyramids");
2578//   linear_sweep(
2579//       rect(50), texture=tex, h=40,
2580//       tex_size=[10,10], style="convex"
2581//   );
2582// Example(3D): "pyramids_vnf" texture.  Slower, but more consistent around complex curves.
2583//   tex = texture("pyramids_vnf");
2584//   linear_sweep(
2585//       rect(50), texture=tex, h=40,
2586//       tex_size=[10,10]
2587//   );
2588// Example(3D): "trunc_pyramids" texture.
2589//   tex = texture("trunc_pyramids");
2590//   linear_sweep(
2591//       rect(50), texture=tex, h=40,
2592//       tex_size=[10,10], style="convex"
2593//   );
2594// Example(3D): "trunc_pyramids_vnf" texture.  Slower, but more consistent around complex curves.
2595//   tex = texture("trunc_pyramids_vnf");
2596//   linear_sweep(
2597//       rect(50), texture=tex, h=40,
2598//       tex_size=[10,10]
2599//   );
2600// Example(3D): "hills" texture.
2601//   tex = texture("hills");
2602//   linear_sweep(
2603//       rect(50), texture=tex, h=40,
2604//       tex_size=[10,10], style="quincunx"
2605//   );
2606// Example(3D): "dots" texture.
2607//   tex = texture("dots");
2608//   linear_sweep(
2609//       rect(50), texture=tex, h=40, tex_scale=1,
2610//       tex_size=[10,10]
2611//   );
2612// Example(3D): "dimples" texture.
2613//   tex = texture("dimples");
2614//   linear_sweep(
2615//       rect(50), texture=tex, h=40, tex_scale=1,
2616//       tex_size=[10,10]
2617//   );
2618// Example(3D): "cones" texture.
2619//   tex = texture("cones");
2620//   linear_sweep(
2621//       rect(50), texture=tex, h=40, tex_scale=3,
2622//       tex_size=[10,10]
2623//   );
2624// Example(3D): "bricks" texture.
2625//   tex = texture("bricks");
2626//   linear_sweep(
2627//       rect(50), texture=tex, h=40,
2628//       tex_size=[10,10]
2629//   );
2630// Example(3D): "bricks_vnf" texture.
2631//   tex = texture("bricks_vnf");
2632//   linear_sweep(
2633//       rect(50), texture=tex, h=40,
2634//       tex_size=[10,10]
2635//   );
2636// Example(3D): "trunc_diamonds" texture.
2637//   tex = texture("trunc_diamonds");
2638//   linear_sweep(
2639//       rect(50), texture=tex, h=40,
2640//       tex_size=[10,10]
2641//   );
2642// Example(3D): "tri_grid" texture.
2643//   tex = texture("tri_grid");
2644//   linear_sweep(
2645//       rect(50), texture=tex, h=40,
2646//       tex_size=[12.5,20]
2647//   );
2648// Example(3D): "hex_grid" texture.
2649//   tex = texture("hex_grid");
2650//   linear_sweep(
2651//       rect(50), texture=tex, h=40,
2652//       tex_size=[12.5,20]
2653//   );
2654// Example(3D): "checkers" texture.
2655//   tex = texture("checkers");
2656//   linear_sweep(
2657//       rect(50), texture=tex, h=40,
2658//       tex_size=[10,10]
2659//   );
2660// Example(3D): "rough" texture.
2661//   tex = texture("rough");
2662//   linear_sweep(
2663//       rect(50), texture=tex, h=40,
2664//       tex_size=[10,10], style="min_edge"
2665//   );
2666
2667function texture(tex, n, inset, gap, roughness) =
2668    tex=="ribs"?
2669        let(
2670            n = quantup(default(n,2),2)
2671        ) [[
2672            each lerpn(1,0,n/2,endpoint=false),
2673            each lerpn(0,1,n/2,endpoint=false),
2674        ]] :
2675    tex=="trunc_ribs"?
2676        let(
2677            n = quantup(default(n,4),4)
2678        ) [[
2679            each repeat(0,n/4),
2680            each lerpn(0,1,n/4,endpoint=false),
2681            each repeat(1,n/4),
2682            each lerpn(1,0,n/4,endpoint=false),
2683        ]] :
2684    tex=="trunc_ribs_vnf"?
2685        let(
2686            inset = default(inset,1/2),
2687            gap = default(gap,1/4)
2688        )
2689        assert(inset >= 0)
2690        assert(gap >= 0)
2691        assert(gap+inset > 0)
2692        assert(gap+inset <= 1)
2693        [
2694            [
2695               each move([0.5,0.5], p=path3d(rect([1-gap,1]),0)),
2696               each move([0.5,0.5], p=path3d(rect([1-gap-inset,1]),1)),
2697               each path3d(square(1)),
2698            ], [
2699                [1,2,6], [1,6,5], [0,4,3], [3,4,7],
2700                if (gap+inset < 1-EPSILON) each [[4,5,6], [4,6,7]],
2701                if (gap > EPSILON) each [[1,9,10], [1,10,2], [0,3,8], [3,11,8]],
2702            ]
2703        ] :
2704    tex=="wave_ribs"?
2705        let(
2706            n = max(6,default(n,8))
2707        ) [[
2708            for(a=[0:360/n:360-EPSILON])
2709            (cos(a)+1)/2
2710        ]] :
2711    tex=="diamonds"?
2712        let(
2713            n = quantup(default(n,2),2)
2714        ) [
2715            let(
2716                path = [
2717                    each lerpn(0,1,n/2,endpoint=false),
2718                    each lerpn(1,0,n/2,endpoint=false),
2719                ]
2720            )
2721            for (i=[0:1:n-1]) [
2722                for (j=[0:1:n-1]) min(
2723                    select(path,i+j),
2724                    select(path,i-j)
2725                )
2726            ],
2727        ] :
2728    tex=="diamonds_vnf"?
2729        [
2730            [
2731                [0,   1, 1], [1/2,   1, 0], [1,   1, 1],
2732                [0, 1/2, 0], [1/2, 1/2, 1], [1, 1/2, 0],
2733                [0,   0, 1], [1/2,   0, 0], [1,   0, 1],
2734            ], [
2735                [0,1,3], [2,5,1], [8,7,5], [6,3,7],
2736                [1,5,4], [5,7,4], [7,3,4], [4,3,1],
2737            ]
2738        ] :
2739    tex=="pyramids"?
2740        let(
2741            n = quantup(default(n,2),2)
2742        ) [
2743            for (i = [0:1:n-1]) [
2744                for (j = [0:1:n-1])
2745                1 - (max(abs(i-n/2), abs(j-n/2)) / (n/2))
2746            ]
2747        ] :
2748    tex=="pyramids_vnf"?
2749        [
2750            [ [0,1,0], [1,1,0], [1/2,1/2,1], [0,0,0], [1,0,0] ],
2751            [ [2,0,1], [2,1,4], [2,4,3], [2,3,0] ]
2752        ] :
2753    tex=="trunc_pyramids"?
2754        let(
2755            n = quantup(default(n,6),3)
2756        ) [
2757            for (i = [0:1:n-1]) [
2758                for (j = [0:1:n-1])
2759                (1 - (max(n/6, abs(i-n/2), abs(j-n/2)) / (n/2))) * 1.5
2760            ]
2761        ] :
2762    tex=="trunc_pyramids_vnf"?
2763        let(
2764            inset = default(inset,0.25)
2765        ) [
2766            [
2767                each path3d(square(1)),
2768                each move([1/2,1/2,1], p=path3d(rect(1-2*inset))),
2769            ], [
2770                for (i=[0:3]) each [
2771                    [i, (i+1)%4, i+4],
2772                    [(i+1)%4, (i+1)%4+4, i+4],
2773                ],
2774                [4,5,6], [4,6,7],
2775            ]
2776        ] :
2777    tex=="hills"?
2778        let(
2779            n = default(n,12)
2780        ) [
2781            for (a=[0:360/n:359.999]) [
2782                for (b=[0:360/n:359.999])
2783                (cos(a)*cos(b)+1)/2
2784            ]
2785        ] :
2786    tex=="bricks"?
2787        let(
2788            n = quantup(default(n,24),2),
2789            rough = default(roughness,0.05)
2790        ) [
2791            for (y = [0:1:n-1])
2792            rands(-rough/2, rough/2, n, seed=12345+y*678) + [
2793                for (x = [0:1:n-1])
2794                (y%(n/2) <= max(1,n/16))? 0 :
2795                let( even = floor(y/(n/2))%2? n/2 : 0 )
2796                (x+even) % n <= max(1,n/16)? 0 : 0.5
2797            ]
2798        ] :
2799    tex=="bricks_vnf"?
2800        let(
2801            inset = default(inset,0.05),
2802            gap = default(gap,0.05)
2803        ) [
2804            [
2805                each path3d(square(1)),
2806                each move([gap/2, gap/2, 0], p=path3d(square([1-gap, 0.5-gap]))),
2807                each move([gap/2+inset/2, gap/2+inset/2, 1], p=path3d(square([1-gap-inset, 0.5-gap-inset]))),
2808                each move([0, 0.5+gap/2, 0], p=path3d(square([0.5-gap/2, 0.5-gap]))),
2809                each move([0, 0.5+gap/2+inset/2, 1], p=path3d(square([0.5-gap/2-inset/2, 0.5-gap-inset]))),
2810                each move([0.5+gap/2, 0.5+gap/2, 0], p=path3d(square([0.5-gap/2, 0.5-gap]))),
2811                each move([0.5+gap/2+inset/2, 0.5+gap/2+inset/2, 1], p=path3d(square([0.5-gap/2-inset/2, 0.5-gap-inset]))),
2812            ], [
2813                [ 8, 9,10], [ 8,10,11], [16,17,18], [16,18,19], [24,25,26],
2814                [24,26,27], [ 0, 1, 5], [ 0, 5, 4], [ 1,13, 6], [ 1, 6, 5],
2815                [ 6,13,12], [ 6,12,21], [ 7,21,20], [ 6,21, 7], [ 0, 4, 7],
2816                [ 0, 7,20], [21,12,15], [21,15,22], [ 3,23,22], [ 3,22,15],
2817                [ 2,15,14], [ 2, 3,15], [23,27,26], [23,26,22], [21,22,26],
2818                [21,26,25], [21,25,24], [21,24,20], [12,16,19], [12,19,15],
2819                [14,15,19], [14,19,18], [13,17,16], [13,16,12], [ 6,10, 9],
2820                [ 6, 9, 5], [ 5, 9, 8], [ 5, 8, 4], [ 4, 8,11], [ 4,11, 7],
2821                [ 7,11,10], [ 7,10, 6],
2822            ]
2823        ] :
2824    tex=="checkers"?
2825        let(
2826            inset = default(inset,0.05)
2827        ) [
2828            [
2829                each move([0,0], p=path3d(square(0.5-inset),1)),
2830                each move([0,0.5], p=path3d(square(0.5-inset))),
2831                each move([0.5,0], p=path3d(square(0.5-inset))),
2832                each move([0.5,0.5], p=path3d(square(0.5-inset),1)),
2833                [1/2-inset/2,1/2-inset/2,1/2], [0,1,1], [1/2-inset,1,1],
2834                [1/2,1,0], [1-inset,1,0], [1,0,1], [1,1/2-inset,1],
2835                [1,1/2,0], [1,1-inset,0], [1,1,1], [1/2-inset/2,1-inset/2,1/2],
2836                [1-inset/2,1-inset/2,1/2], [1-inset/2,1/2-inset/2,1/2],
2837            ], [
2838                for (i=[0:4:12]) each [[i,i+1,i+2], [i, i+2, i+3]],
2839                [10,13,11], [13,12,11], [2,5,4], [4,3,2],
2840                [0,3,10], [10,9,0], [4,7,14], [4,14,13],
2841                [4,13,16], [10,16,13], [10,3,16], [3,4,16],
2842                [7,6,17], [7,17,18], [14,19,20], [14,20,15],
2843                [8,11,22], [8,22,21], [12,15,24], [12,24,23],
2844                [7,18,26], [7,26,14], [14,26,19], [18,19,26],
2845                [15,20,27], [20,25,27], [24,27,25], [15,27,24],
2846                [11,12,28], [12,23,28], [11,28,22], [23,22,28],
2847            ]
2848        ] :
2849    tex=="cones"?
2850        let(
2851            n = quant(default(n,12),4),
2852            inset = default(inset,0)
2853        )
2854        assert(inset>=0 && inset<0.5)
2855        [
2856            [
2857                each move([1/2,1/2], p=path3d(circle(d=1-inset,$fn=n))),
2858                [1/2,1/2,1],
2859                each path3d(square(1)),
2860            ], [
2861                for (i=[0:1:n-1]) [i, (i+1)%n, n],
2862                for (i=[0:1:3], j=[0:1:n/4-1]) [n+1+i, (i*n/4+j+1)%n, i*n/4+j],
2863                if (inset > 0) for (i = [0:1:3]) [i+n+1, (i+1)%4+n+1, ((i+1)*n/4)%n],
2864            ]
2865        ] :
2866    tex=="cubes"?
2867        [
2868            [
2869                [0,1,1/2], [1,1,1/2], [1/2,5/6,1], [0,4/6,0], [1,4/6,0],
2870                [1/2,3/6,1/2], [0,2/6,1], [1,2/6,1], [1/2,1/6,0], [0,0,1/2],
2871                [1,0,1/2],
2872            ], [
2873                [0,1,2], [0,2,3], [1,4,2], [2,5,3], [2,4,5],
2874                [6,3,5], [4,7,5], [7,8,5], [6,5,8], [10,8,7],
2875                [9,6,8], [10,9,8],
2876            ]
2877        ] :
2878    tex=="trunc_diamonds"?
2879        let(
2880            inset = default(inset,0.1)
2881        )
2882        assert(inset>0 && inset<0.5)
2883        [
2884            [
2885                each move([1/2,1/2,0], p=path3d(circle(d=1,$fn=4))),
2886                each move([1/2,1/2,1], p=path3d(circle(d=1-inset*2,$fn=4))),
2887                for (a=[0:90:359]) each move([1/2,1/2], p=zrot(-a, p=[[1/2,inset,1], [inset,1/2,1], [1/2,1/2,1]]))
2888            ], [
2889                for (i=[0:3]) each let(j=i*3+8) [
2890                    [i,(i+1)%4,(i+1)%4+4], [i,(i+1)%4+4,i+4],
2891                    [j,j+1,j+2], [i, (i+3)%4, j], [(i+3)%4, j+1, j],
2892                ],
2893                [4,5,6], [4,6,7],
2894            ]
2895        ] :
2896    tex=="dimples" || tex=="dots" ?
2897        let(
2898            n = quant(default(n,16),4),
2899            inset = default(inset,0.05)
2900        )
2901        assert(inset>=0 && inset < 0.5)
2902        let(
2903            rows=ceil(n/4),
2904            r=adj_ang_to_hyp(1/2-inset,45),
2905            dots = tex=="dots",
2906            cp = [1/2, 1/2, r*sin(45)*(dots?-1:1)],
2907            sc = 1 / (r - abs(cp.z)),
2908            uverts = [
2909                each path3d(square(1)),
2910                for (p=[0:1:rows-1], t=[0:360/n:359.999])
2911                    cp + (
2912                        dots? spherical_to_xyz(r, -t, 45-45*p/rows) :
2913                        spherical_to_xyz(r, -t, 135+45*p/rows)
2914                    ),
2915                cp + r * (dots?UP:DOWN),
2916            ],
2917            verts = zscale(sc, p=uverts),
2918            faces = [
2919                for (i=[0:1:3], j=[0:1:n/4-1]) [i, 4+(i*n/4+j+1)%n, 4+i*n/4+j],
2920                for (i=[0:1:rows-2], j=[0:1:n-1]) each [
2921                    [4+i*n+j, 4+(i+1)*n+(j+1)%n, 4+(i+1)*n+j],
2922                    [4+i*n+j, 4+i*n+(j+1)%n, 4+(i+1)*n+(j+1)%n],
2923                ],
2924                for (i=[0:1:n-1]) [4+(rows-1)*n+i, 4+(rows-1)*n+(i+1)%n, 4+rows*n],
2925                if (inset>0) for (i=[0:3]) [i, (i+1)%4, 4+(i+1)%4*n/4]
2926            ]
2927        ) [verts, faces] :
2928    tex=="tri_grid"?
2929        let(
2930            inset = default(inset,0.1),
2931            aspect = 1 / adj_ang_to_opp(1,60),
2932            adj = opp_ang_to_adj(inset, 30),
2933            hyp = opp_ang_to_hyp(inset, 30),
2934            y1 = inset * aspect,
2935            y2 = adj * aspect,
2936            y3 = 0.5 - inset * aspect,
2937            y4 = 0.5 + inset * aspect,
2938            y5 = 1 - adj * aspect,
2939            y6 = 1 - inset * aspect
2940        )
2941        assert(inset>0 && inset<0.5)
2942        [
2943            [
2944                [0,0,0], [1,0,0],
2945                [adj,y1,1], [1-adj,y1,1],
2946                [0,y2,1], [1,y2,1],
2947                [0.5,0.5-adj*aspect,1],
2948                [0,y3,1], [0.5-adj,y3,1], [0.5+adj,y3,1], [1,y3,1],
2949                [0,0.5,0], [0.5,0.5,0], [1,0.5,0],
2950                [0,y4,1], [0.5-adj,y4,1], [0.5+adj,y4,1], [1,y4,1],
2951                [0.5,0.5+adj*aspect,1],
2952                [0,y5,1], [1,y5,1],
2953                [adj,y6,1], [1-adj,y6,1],
2954                [0,1,0], [1,1,0],
2955            ], [
2956               [0,2,3], [0,3,1], [2,6,3], [0,12,2], [2,12,6], [3,6,12], [3,12,1],
2957               [0,4,8], [0,8,12], [4,7,8], [7,11,12], [7,12,8],
2958               [1,12,9], [1,9,5], [5,9,10], [9,12,13], [9,13,10],
2959               [11,14,15], [11,15,12], [19,15,14], [19,23,12], [19,12,15],
2960               [12,16,13], [16,17,13], [16,20,17], [12,24,20], [12,20,16],
2961               [21,22,18], [21,23,24], [21,24,22], [12,23,21], [12,21,18],
2962               [12,18,22], [12,22,24],
2963            ]
2964        ] :
2965    tex=="hex_grid"?
2966        let(
2967            inset=default(inset,0.1)
2968        )
2969        assert(inset>=0 && inset<0.5)
2970        let(
2971            diag=opp_ang_to_hyp(inset,60),
2972            side=adj_ang_to_opp(1,30),
2973            hyp=adj_ang_to_hyp(0.5,30),
2974            sc = 1/3/hyp,
2975            hex=[ [1,2/6,0], [1/2,1/6,0], [0,2/6,0], [0,4/6,0], [1/2,5/6,0], [1,4/6,0] ]
2976        ) [
2977            [
2978                each hex,
2979                each move([0.5,0.5], p=yscale(sc, p=path3d(ellipse(d=1-2*inset, circum=true, spin=-30,$fn=6),1))),
2980                hex[0]-[0,diag*sc,-1],
2981                for (ang=[270+60,270-60]) hex[1]+yscale(sc, p=cylindrical_to_xyz(diag,ang,1)),
2982                hex[2]-[0,diag*sc,-1],
2983                [0,0,1], [0.5-inset,0,1], [0.5,0,0], [0.5+inset,0,1], [1,0,1],
2984                hex[3]+[0,diag*sc,1],
2985                for (ang=[90+60,90-60]) hex[4]+yscale(sc, p=cylindrical_to_xyz(diag,ang,1)),
2986                hex[5]+[0,diag*sc,1],
2987                [0,1,1], [0.5-inset,1,1], [0.5,1,0], [0.5+inset,1,1], [1,1,1],
2988            ], [
2989                for (i=[0:2:5]) let(b=6) [b+i, b+(i+1)%6, b+(i+2)%6], [6,8,10],
2990                for (i=[0:1:5]) each [ [i, (i+1)%6, (i+1)%6+6], [i, (i+1)%6+6, i+6] ],
2991                [19,13,12], [19,12,20], [17,16,15], [17,15,14],
2992                [21,25,26], [21,26,22], [23,28,29], [23,29,24],
2993                [0,12,13], [0,13,1], [1,14,15], [1,15,2],
2994                [3,21,22], [3,22,4], [4,23,24], [4,24,5],
2995                [1,13,19], [1,19,18], [1,18,17], [1,17,14],
2996                [4,22,26], [4,26,27], [4,27,28], [4,28,23],
2997            ]
2998        ] :
2999    tex=="rough"?
3000        let(
3001            n = default(n,32),
3002            rough = default(roughness, 0.2)
3003        ) [
3004            for (y = [0:1:n-1])
3005            rands(0, rough, n, seed=123456+29*y)
3006        ] :
3007    assert(false, str("Unrecognized texture name: ", tex));
3008
3009
3010/// Function&Module: _textured_linear_sweep()
3011/// Usage: As Function
3012///   vnf = _textured_linear_sweep(region, texture, tex_size, h, ...);
3013///   vnf = _textured_linear_sweep(region, texture, counts=, h=, ...);
3014/// Usage: As Module
3015///   _textured_linear_sweep(region, texture, tex_size, h, ...) [ATTACHMENTS];
3016///   _textured_linear_sweep(region, texture, counts=, h=, ...) [ATTACHMENTS];
3017/// Topics: Sweep, Extrusion, Textures, Knurling
3018/// Description:
3019///   Given a [[Region|regions.scad]], creates a linear extrusion of it vertically, optionally twisted, scaled, and/or shifted,
3020///   with a given texture tiled evenly over the side surfaces.  The texture can be given in one of three ways:
3021///   - As a texture name string. (See {{texture()}} for supported named textures.)
3022///   - As a 2D array of evenly spread height values. (AKA a heightfield.)
3023///   - As a VNF texture tile.  A VNF tile exactly defines a surface from `[0,0]` to `[1,1]`, with the Z coordinates
3024///     being the height of the texture point from the surface.  VNF tiles MUST be able to tile in both X and Y
3025///     directions with no gaps, with the front and back edges aligned exactly, and the left and right edges as well.
3026///   One script to convert a grayscale image to a texture heightfield array in a .scad file can be found at:
3027///   https://raw.githubusercontent.com/revarbat/BOSL2/master/scripts/img2scad.py
3028/// Arguments:
3029///   region = The [[Region|regions.scad]] to sweep/extrude.
3030///   texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to vertical surfaces.  See {{texture()}} for what named textures are supported.
3031///   tex_size = An optional 2D target size for the textures.  Actual texture sizes will be scaled somewhat to evenly fit the available surface. Default: `[5,5]`
3032///   h / l = The height to extrude/sweep the path.
3033///   ---
3034///   counts = If given instead of tex_size, gives the tile repetition counts for textures over the surface length and height.
3035///   inset = If numeric, lowers the texture into the surface by that amount, before the tex_scale multiplier is applied.  If `true`, insets by exactly `1`.  Default: `false`
3036///   rot = If true, rotates the texture 90º.
3037///   tex_scale = Scaling multiplier for the texture depth.
3038///   twist = Degrees of twist for the top of the extrustion/sweep, compared to the bottom.  Default: 0
3039///   scale = Scaling multiplier for the top of the extrustion/sweep, compared to the bottom.  Default: 1
3040///   shift = [X,Y] amount to translate the top, relative to the bottom.  Default: [0,0]
3041///   style = The triangulation style used.  See {{vnf_vertex_array()}} for valid styles.  Used only with heightfield type textures. Default: `"min_edge"`
3042///   samples = Minimum number of "bend points" to have in VNF texture tiles.  Default: 8
3043///   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
3044///   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
3045///   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
3046/// Extra Anchors:
3047///   centroid_top = The centroid of the top of the shape, oriented UP.
3048///   centroid = The centroid of the center of the shape, oriented UP.
3049///   centroid_bot = The centroid of the bottom of the shape, oriented DOWN.
3050/// See Also: heightfield(), cylindrical_heightfield(), texture()
3051
3052function _get_vnf_tile_edges(texture) =
3053    let(
3054        verts = texture[0],
3055        faces = texture[1],
3056        everts = [for (v = verts) (v.x==0 || v.y==0 || v.x==1 || v.y==1)],
3057        uc = unique_count([
3058            for (face = faces, i = idx(face))
3059            let(edge = select(face,i,i+1), i1 = min(edge), i2 = max(edge))
3060            if (everts[i1] && everts[i2])
3061            [i1, i2]
3062        ]),
3063        edges = uc[0], counts = uc[1],
3064        uedges = [for (i = idx(edges)) if (counts[i] == 1) edges[i] ]
3065    ) uedges;
3066
3067
3068function _validate_texture(texture) =
3069    is_vnf(texture)
3070      ? let( // Validate VNF tile texture
3071            bounds = pointlist_bounds(texture[0]),
3072            min_xy = point2d(bounds[0]),
3073            max_xy = point2d(bounds[1])
3074        )
3075        assert(min_xy==[0,0] && max_xy==[1,1], "VNF tiles must span exactly from [0,0] to [1,1] in the X and Y components.")
3076        let(
3077            verts = texture[0],
3078            uedges = _get_vnf_tile_edges(texture),
3079            edge_verts = [for (i = unique(flatten(uedges))) verts[i] ],
3080            hverts = [for(v = edge_verts) if(v.x==0 || v.x==1) v],
3081            vverts = [for(v = edge_verts) if(v.y==0 || v.y==1) v],
3082            allgoodx = all(hverts, function(v) any(hverts, function(w) w==[1-v.x, v.y, v.z])),
3083            allgoody = all(vverts, function(v) any(vverts, function(w) w==[v.x, 1-v.y, v.z]))
3084        )
3085        assert(allgoodx && allgoody, "All VNF tile edge vertices must line up with a vertex on the opposite side of the tile.")
3086        true
3087      : // Validate heightfield texture.
3088        assert(is_matrix(texture), "Malformed texture.")
3089        let( tex_dim = list_shape(texture) )
3090        assert(len(tex_dim) == 2, "Heightfield texture must be a 2D square array of scalar heights.")
3091        assert(all_defined(tex_dim), "Heightfield texture must be a 2D square array of scalar heights.")
3092        true;
3093
3094
3095function _textured_linear_sweep(
3096    region, texture, tex_size=[5,5],
3097    h, counts, inset=false, rot=false,
3098    tex_scale=1, twist, scale, shift,
3099    style="min_edge", l,
3100    height, length, samples,
3101    anchor=CENTER, spin=0, orient=UP
3102) =
3103    assert(is_path(region,[2]) || is_region(region))
3104    assert(is_undef(samples) || is_int(samples))
3105    assert(counts==undef || is_vector(counts,2))
3106    assert(tex_size==undef || is_vector(tex_size,2))
3107    assert(is_bool(rot) || in_list(rot,[0,90,180,270]))
3108    let(
3109        regions = is_path(region,2)? [[region]] : region_parts(region),
3110        tex = is_string(texture)? texture(texture) : texture,
3111        texture = !rot? tex :
3112            is_vnf(tex)? zrot(is_num(rot)?rot:90, cp=[1/2,1/2], p=tex) :
3113            rot==180? reverse([for (row=tex) reverse(row)]) :
3114            rot==270? [for (row=transpose(tex)) reverse(row)] :
3115            reverse(transpose(tex)),
3116        h = first_defined([h, l, height, length, 1]),
3117        inset = is_num(inset)? inset : inset? 1 : 0,
3118        twist = default(twist, 0),
3119        shift = default(shift, [0,0]),
3120        scale = scale==undef? [1,1,1] :
3121            is_num(scale)? [scale,scale,1] : scale,
3122        samples = !is_vnf(texture)? len(texture[0]) :
3123            is_num(samples)? samples : 8,
3124        check_tex = _validate_texture(texture),
3125        sorted_tile =
3126            !is_vnf(texture)? texture :
3127            let(
3128                s = 1 / max(1, samples),
3129                vnf = samples<=1? texture :
3130                    let(
3131                        vnft = vnf_slice(texture, "X", list([s:s:1-s/2])),
3132                        zvnf = [
3133                            [
3134                                for (p=vnft[0]) [
3135                                    approx(p.x,0)? 0 : approx(p.x,1)? 1 : p.x,
3136                                    approx(p.y,0)? 0 : approx(p.y,1)? 1 : p.y,
3137                                    p.z
3138                                ]
3139                            ],
3140                            vnft[1]
3141                        ]
3142                    ) zvnf
3143            ) _vnf_sort_vertices(vnf, idx=[1,0]),
3144        vertzs = !is_vnf(sorted_tile)? undef :
3145            group_sort(sorted_tile[0], idx=1),
3146        tpath = is_vnf(sorted_tile)
3147            ? _find_vnf_tile_edge_path(sorted_tile,0)
3148            : let(
3149                  row = sorted_tile[0],
3150                  rlen = len(row)
3151              ) [for (i = [0:1:rlen]) [i/rlen, row[i%rlen]]],
3152        tmat = scale(scale) * zrot(twist) * up(h/2),
3153        pre_skew_vnf = vnf_join([
3154            for (rgn = regions) let(
3155                walls_vnf = vnf_join([
3156                    for (path = rgn) let(
3157                        path = reverse(path),
3158                        plen = path_length(path, closed=true),
3159                        counts = is_vector(counts,2)? counts :
3160                            is_vector(tex_size,2)
3161                              ? [round(plen/tex_size.x), max(1,round(h/tex_size.y)), ]
3162                              : [ceil(6*plen/h), 6],
3163                        obases = resample_path(path, n=counts.x * samples, closed=true),
3164                        onorms = path_normals(obases, closed=true),
3165                        bases = close_path(obases),
3166                        norms = close_path(onorms),
3167                        vnf = is_vnf(texture)
3168                          ? let( // VNF tile texture
3169                                row_vnf = vnf_join([
3170                                    for (j = [0:1:counts.x-1]) [
3171                                        [
3172                                            for (group = vertzs)
3173                                            each [
3174                                                for (vert = group) let(
3175                                                    u = floor((j + vert.x) * samples),
3176                                                    uu = ((j + vert.x) * samples) - u,
3177                                                    texh = (vert.z - inset) * tex_scale,
3178                                                    base = lerp(bases[u], select(bases,u+1), uu),
3179                                                    norm = unit(lerp(norms[u], select(norms,u+1), uu)),
3180                                                    xy = base + norm * texh
3181                                                ) point3d(xy,vert.y)
3182                                            ]
3183                                        ],
3184                                        sorted_tile[1]
3185                                    ]
3186                                ]),
3187                                sorted_row = _vnf_sort_vertices(row_vnf, idx=[1,0]),
3188                                rvertzs = group_sort(sorted_row[0], idx=1),
3189                                vnf1 = vnf_join([
3190                                    for (i = [0:1:counts.y-1]) [
3191                                        [
3192                                            for (group = rvertzs) let(
3193                                                v = (i + group[0].z) / counts.y,
3194                                                sc = lerp([1,1,1], scale, v),
3195                                                mat = scale(sc) *
3196                                                    zrot(twist*v) *
3197                                                    up(((i/counts.y)-0.5)*h) *
3198                                                    zscale(h/counts.y)
3199                                            ) each apply(mat, group)
3200                                        ],
3201                                        sorted_row[1]
3202                                    ]
3203                                ])
3204                            ) vnf1
3205                          : let( // Heightfield texture
3206                                texcnt = [len(texture[0]), len(texture)],
3207                                tile_rows = [
3208                                    for (ti = [0:1:texcnt.y-1])
3209                                    path3d([
3210                                        for (j = [0:1:counts.x])
3211                                        for (tj = [0:1:texcnt.x-1])
3212                                        if (j != counts.x || tj == 0)
3213                                        let(
3214                                            part = (j + (tj/texcnt.x)) * samples,
3215                                            u = floor(part),
3216                                            uu = part - u,
3217                                            texh = (texture[ti][tj] - inset) * tex_scale,
3218                                            base = lerp(bases[u], select(bases,u+1), uu),
3219                                            norm = unit(lerp(norms[u], select(norms,u+1), uu)),
3220                                            xy = base + norm * texh
3221                                        ) xy
3222                                    ])
3223                                ],
3224                                tiles = [
3225                                    for (i = [0:1:counts.y], ti = [0:1:texcnt.y-1])
3226                                    if (i != counts.y || ti == 0)
3227                                    let(
3228                                        v = (i + (ti/texcnt.y)) / counts.y,
3229                                        sc = lerp([1,1,1], scale, v),
3230                                        mat = down((v-0.5)*h) *
3231                                              scale(sc) *
3232                                              zrot(twist*v)
3233                                    ) apply(mat, tile_rows[ti])
3234                                ]
3235                            ) vnf_vertex_array(
3236                                tiles, caps=false, style=style,
3237                                col_wrap=true, row_wrap=false
3238                            )
3239                    ) vnf
3240                ]),
3241                brgn = [
3242                    for (path = rgn) let(
3243                        path = reverse(path),
3244                        plen = path_length(path, closed=true),
3245                        counts = is_vector(counts,2)? counts :
3246                            is_vector(tex_size,2)
3247                              ? [round(plen/tex_size.x), max(1,round(h/tex_size.y)), ]
3248                              : [ceil(6*plen/h), 6],
3249                        obases = resample_path(path, n=counts.x * samples, closed=true),
3250                        onorms = path_normals(obases, closed=true),
3251                        bases = close_path(obases),
3252                        norms = close_path(onorms),
3253                        nupath = [
3254                            for (j = [0:1:counts.x-1], vert = tpath) let(
3255                                part = (j + vert.x) * samples,
3256                                u = floor(part),
3257                                uu = part - u,
3258                                texh = (vert.y - inset) * tex_scale,
3259                                base = lerp(bases[u], select(bases,u+1), uu),
3260                                norm = unit(lerp(norms[u], select(norms,u+1), uu)),
3261                                xy = base + norm * texh
3262                            ) xy
3263                        ]
3264                    ) nupath
3265                ],
3266                bot_vnf = vnf_from_region(brgn, down(h/2), reverse=true),
3267                top_vnf = vnf_from_region(brgn, tmat, reverse=false)
3268            ) vnf_join([walls_vnf, bot_vnf, top_vnf])
3269        ]),
3270        skmat = down(h/2) * skew(sxz=shift.x/h, syz=shift.y/h) * up(h/2),
3271        final_vnf = apply(skmat, pre_skew_vnf),
3272        cent = centroid(region),
3273        anchors = [
3274            named_anchor("centroid_top", point3d(cent, h/2), UP),
3275            named_anchor("centroid",     point3d(cent),      UP),
3276            named_anchor("centroid_bot", point3d(cent,-h/2), DOWN)
3277        ]
3278    ) reorient(anchor,spin,orient, vnf=final_vnf, extent=true, anchors=anchors, p=final_vnf);
3279
3280
3281module _textured_linear_sweep(
3282    path, texture, tex_size=[5,5], h,
3283    inset=false, rot=false, tex_scale=1,
3284    twist, scale, shift, samples,
3285    style="min_edge", l,
3286    height, length, counts,
3287    anchor=CENTER, spin=0, orient=UP,
3288    convexity=10
3289) {
3290    h = first_defined([h, l, height, length, 1]);
3291    vnf = _textured_linear_sweep(
3292        path, texture, h=h,
3293        tex_size=tex_size, counts=counts,
3294        inset=inset, rot=rot, tex_scale=tex_scale,
3295        twist=twist, scale=scale, shift=shift,
3296        samples=samples, style=style,
3297        anchor=CENTER, spin=0, orient=UP
3298    );
3299    cent = centroid(path);
3300    anchors = [
3301        named_anchor("centroid_top", point3d(cent, h/2), UP),
3302        named_anchor("centroid",     point3d(cent),      UP),
3303        named_anchor("centroid_bot", point3d(cent,-h/2), DOWN)
3304    ];
3305    attachable(anchor,spin,orient, vnf=vnf, extent=true, anchors=anchors) {
3306        vnf_polyhedron(vnf, convexity=convexity);
3307        children();
3308    }
3309}
3310
3311function _find_vnf_tile_edge_path(vnf, val) =
3312    let(
3313        verts = vnf[0],
3314        fragments = [
3315            for(edge = _get_vnf_tile_edges(vnf))
3316            let(v0 = verts[edge[0]], v1 = verts[edge[1]])
3317            if (approx(v0.y, val) && approx(v1.y, val))
3318            v0.x <= v1.x? [[v0.x,v0.z], [v1.x,v1.z]] :
3319            [[v1.x,v1.z], [v0.x,v0.z]]
3320        ],
3321        sfrags = sort(fragments, idx=[0,1]),
3322        rpath = _assemble_a_path_from_fragments(sfrags)[0],
3323        opath = rpath[0].x > last(rpath).x? reverse(rpath) : rpath
3324    ) opath;
3325
3326
3327/// Function&Module: _textured_revolution()
3328/// Usage: As Function
3329///   vnf = _textured_revolution(shape, texture, tex_size, [tex_scale=], ...);
3330///   vnf = _textured_revolution(shape, texture, counts=, [tex_scale=], ...);
3331/// Usage: As Module
3332///   _textured_revolution(shape, texture, tex_size, [tex_scale=], ...) [ATTACHMENTS];
3333///   _textured_revolution(shape, texture, counts=, [tex_scale=], ...) [ATTACHMENTS];
3334/// Topics: Sweep, Extrusion, Textures, Knurling
3335/// Description:
3336///   Given a 2D region or path, fully in the X+ half-plane, revolves that shape around the Z axis (after rotating its Y+ to Z+).
3337///   This creates a solid from that surface of revolution, possibly capped top and bottom, with the sides covered in a given tiled texture.
3338///   The texture can be given in one of three ways:
3339///   - As a texture name string. (See {{texture()}} for supported named textures.)
3340///   - As a 2D array of evenly spread height values. (AKA a heightfield.)
3341///   - As a VNF texture tile.  A VNF tile exactly defines a surface from `[0,0]` to `[1,1]`, with the Z coordinates
3342///     being the height of the texture point from the surface.  VNF tiles MUST be able to tile in both X and Y
3343///     directions with no gaps, with the front and back edges aligned exactly, and the left and right edges as well.
3344///   One script to convert a grayscale image to a texture heightfield array in a .scad file can be found at:
3345///   https://raw.githubusercontent.com/revarbat/BOSL2/master/scripts/img2scad.py
3346/// Arguments:
3347///   shape = The path or region to sweep/extrude.
3348///   texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to the revolution surface.  See {{texture()}} for what named textures are supported.
3349///   tex_size = An optional 2D target size for the textures.  Actual texture sizes will be scaled somewhat to evenly fit the available surface. Default: `[5,5]`
3350///   tex_scale = Scaling multiplier for the texture depth.
3351///   ---
3352///   inset = If numeric, lowers the texture into the surface by that amount, before the tex_scale multiplier is applied.  If `true`, insets by exactly `1`.  Default: `false`
3353///   rot = If true, rotates the texture 90º.
3354///   shift = [X,Y] amount to translate the top, relative to the bottom.  Default: [0,0]
3355///   closed = If false, and shape is given as a path, then the revolved path will be sealed to the axis of rotation with untextured caps.  Default: `true`
3356///   taper = If given, and `closed=false`, tapers the texture height to zero over the first and last given percentage of the path.  If given as a lookup table with indices between 0 and 100, uses the percentage lookup table to ramp the texture heights.  Default: `undef` (no taper)
3357///   angle = The number of degrees counter-clockwise from X+ to revolve around the Z axis.  Default: `360`
3358///   style = The triangulation style used.  See {{vnf_vertex_array()}} for valid styles.  Used only with heightfield type textures. Default: `"min_edge"`
3359///   counts = If given instead of tex_size, gives the tile repetition counts for textures over the surface length and height.
3360///   samples = Minimum number of "bend points" to have in VNF texture tiles.  Default: 8
3361///   anchor = Translate so anchor point is at origin (0,0,0).  See [anchor](attachments.scad#subsection-anchor).  Default: `CENTER`
3362///   spin = Rotate this many degrees around the Z axis after anchor.  See [spin](attachments.scad#subsection-spin).  Default: `0`
3363///   orient = Vector to rotate top towards, after spin.  See [orient](attachments.scad#subsection-orient).  Default: `UP`
3364/// See Also: heightfield(), cylindrical_heightfield(), texture()
3365/// Anchor Types:
3366///   "hull" = Anchors to the virtual convex hull of the shape.
3367///   "intersect" = Anchors to the surface of the shape.
3368
3369function _textured_revolution(
3370    shape, texture, tex_size, tex_scale=1,
3371    inset=false, rot=false, shift=[0,0],
3372    taper, closed=true, angle=360,
3373    counts, samples,
3374    style="min_edge", atype="intersect",
3375    anchor=CENTER, spin=0, orient=UP
3376) =
3377    assert(angle>0 && angle<=360)
3378    assert(is_path(shape,[2]) || is_region(shape))
3379    assert(is_undef(samples) || is_int(samples))
3380    assert(is_bool(closed))
3381    assert(counts==undef || is_vector(counts,2))
3382    assert(tex_size==undef || is_vector(tex_size,2))
3383    assert(is_bool(rot) || in_list(rot,[0,90,180,270]))
3384    let( taper_is_ok = is_undef(taper) || (is_finite(taper) && taper>=0 && taper<50) || is_path(taper,2) )
3385    assert(taper_is_ok, "Bad taper= value.")
3386    assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"")
3387    let(
3388        regions = !is_path(shape,2)? region_parts(shape) :
3389            shape[0].y <= last(shape).y? [[reverse(shape)]] :
3390            [[shape]],
3391        checks = [
3392            for (rgn=regions, path=rgn)
3393            assert(all(path, function(pt) pt.x>=0))
3394        ]
3395    )
3396    assert(closed || is_path(shape,2))
3397    let(
3398        tex = is_string(texture)? texture(texture) : texture,
3399        texture = !rot? tex :
3400            is_vnf(tex)? zrot(is_num(rot)?rot:90, cp=[1/2,1/2], p=tex) :
3401            rot==180? reverse([for (row=tex) reverse(row)]) :
3402            rot==270? [for (row=transpose(tex)) reverse(row)] :
3403            reverse(transpose(tex)),
3404        check_tex = _validate_texture(texture),
3405        inset = is_num(inset)? inset : inset? 1 : 0,
3406        samples = !is_vnf(texture)? len(texture) :
3407            is_num(samples)? samples : 8,
3408        bounds = pointlist_bounds(flatten(flatten(regions))),
3409        maxx = bounds[1].x,
3410        miny = bounds[0].y,
3411        maxy = bounds[1].y,
3412        h = maxy - miny,
3413        circumf = 2 * PI * maxx,
3414        tile = !is_vnf(texture)? texture :
3415            let(
3416                utex = samples<=1? texture :
3417                    let(
3418                        s = 1 / samples,
3419                        slices = list([s : s : 1-s/2]),
3420                        vnfx = vnf_slice(texture, "X", slices),
3421                        vnfy = vnf_slice(vnfx, "Y", slices),
3422                        vnft = vnf_triangulate(vnfy),
3423                        zvnf = [
3424                            [
3425                                for (p=vnft[0]) [
3426                                    approx(p.x,0)? 0 : approx(p.x,1)? 1 : p.x,
3427                                    approx(p.y,0)? 0 : approx(p.y,1)? 1 : p.y,
3428                                    p.z
3429                                ]
3430                            ],
3431                            vnft[1]
3432                        ]
3433                    ) zvnf
3434            ) _vnf_sort_vertices(utex, idx=[0,1]),
3435        vertzs = is_vnf(texture)? group_sort(tile[0], idx=0) : undef,
3436        bpath = is_vnf(tile)
3437            ? _find_vnf_tile_edge_path(tile,1)
3438            : let(
3439                  row = tile[0],
3440                  rlen = len(row)
3441              ) [for (i = [0:1:rlen]) [i/rlen, row[i%rlen]]],
3442        counts_x = is_vector(counts,2)? counts.x :
3443            is_vector(tex_size,2)
3444              ? max(1,round(angle/360*circumf/tex_size.x))
3445              : ceil(6*angle/360*circumf/h),
3446        taper_lup = closed || is_undef(taper)? [[-1,1],[2,1]] :
3447            is_num(taper)? [[-1,0], [0,0], [taper/100+EPSILON,1], [1-taper/100-EPSILON,1], [1,0], [2,0]] :
3448            is_path(taper,2)? let(
3449                retaper = [
3450                    for (t=taper)
3451                    assert(t[0]>=0 && t[0]<=100, "taper lookup indices must be betweem 0 and 100 inclusive.")
3452                    [t[0]/100, t[1]]
3453                ],
3454                taperout = [[-1,retaper[0][1]], each retaper, [2,last(retaper)[1]]]
3455            ) taperout :
3456            assert(false, "Bad taper= argument value."),
3457        full_vnf = vnf_join([
3458            for (rgn = regions) let(
3459                rgn_wall_vnf = vnf_join([
3460                    for (path = rgn) let(
3461                        plen = path_length(path, closed=closed),
3462                        counts_y = is_vector(counts,2)? counts.y :
3463                            is_vector(tex_size,2)? max(1,round(plen/tex_size.y)) : 6,
3464                        obases = resample_path(path, n=counts_y * samples + (closed?0:1), closed=closed),
3465                        onorms = path_normals(obases, closed=closed),
3466                        rbases = closed? close_path(obases) : obases,
3467                        rnorms = closed? close_path(onorms) : onorms,
3468                        bases = xrot(90, p=path3d(rbases)),
3469                        norms = xrot(90, p=path3d(rnorms)),
3470                        vnf = is_vnf(texture)
3471                          ? vnf_join([ // VNF tile texture
3472                                for (j = [0:1:counts_y-1])
3473                                [
3474                                    [
3475                                        for (group = vertzs) each [
3476                                            for (vert = group) let(
3477                                                part = (j + (1-vert.y)) * samples,
3478                                                u = floor(part),
3479                                                uu = part - u,
3480                                                base = lerp(select(bases,u), select(bases,u+1), uu),
3481                                                norm = unit(lerp(select(norms,u), select(norms,u+1), uu)),
3482                                                tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
3483                                                texh = (vert.z - inset) * tex_scale * (base.x / maxx),
3484                                                xyz = base - norm * texh
3485                                            ) zrot(vert.x*angle/counts_x, p=xyz)
3486                                        ]
3487                                    ],
3488                                    tile[1]
3489                                ]
3490                            ])
3491                          : let( // Heightfield texture
3492                                texcnt = [len(texture[0]), len(texture)],
3493                                tiles = transpose([
3494                                    for (j = [0,1], tj = [0:1:texcnt.x-1])
3495                                    if (j == 0 || tj == 0)
3496                                    let(
3497                                        v = (j + (tj/texcnt.x)) / counts_x,
3498                                        mat = zrot(v*angle)
3499                                    ) apply(mat, [
3500                                        for (i = [0:1:counts_y-(closed?1:0)], ti = [0:1:texcnt.y-1])
3501                                        if (i != counts_y || ti == 0)
3502                                        let(
3503                                            part = (i + (ti/texcnt.y)) * samples,
3504                                            u = floor(part),
3505                                            uu = part - u,
3506                                            base = lerp(bases[u], select(bases,u+1), uu),
3507                                            norm = unit(lerp(norms[u], select(norms,u+1), uu)),
3508                                            tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
3509                                            texh = (texture[ti][tj] - inset) * tex_scale * (base.x / maxx),
3510                                            xyz = base - norm * texh
3511                                        ) xyz
3512                                    ])
3513                                ])
3514                            ) vnf_vertex_array(
3515                                tiles, caps=false, style=style,
3516                                col_wrap=false, row_wrap=closed
3517                            )
3518                    ) vnf
3519                ]),
3520                walls_vnf = vnf_join([
3521                    for (i = [0:1:counts_x-1])
3522                    zrot(i*angle/counts_x, rgn_wall_vnf)
3523                ]),
3524                endcap_vnf = angle == 360? EMPTY_VNF :
3525                    let(
3526                        cap_rgn = [
3527                            for (path = rgn) let(
3528                                plen = path_length(path, closed=closed),
3529                                counts_y = is_vector(counts,2)? counts.y :
3530                                    is_vector(tex_size,2)? max(1,round(plen/tex_size.y)) : 6,
3531                                obases = resample_path(path, n=counts_y * samples + (closed?0:1), closed=closed),
3532                                onorms = path_normals(obases, closed=closed),
3533                                bases = closed? close_path(obases) : obases,
3534                                norms = closed? close_path(onorms) : onorms,
3535                                ppath = is_vnf(texture)
3536                                  ? [ // VNF tile texture
3537                                        for (j = [0:1:counts_y-1])
3538                                        for (group = vertzs, vert = reverse(group))
3539                                        if (approx(vert.x, 0)) let(
3540                                            part = (j + (1 - vert.y)) * samples,
3541                                            u = floor(part),
3542                                            uu = part - u,
3543                                            base = lerp(select(bases,u), select(bases,u+1), uu),
3544                                            norm = unit(lerp(select(norms,u), select(norms,u+1), uu)),
3545                                            tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
3546                                            texh = (vert.z - inset) * tex_scale * (base.x / maxx),
3547                                            xyz = base - norm * texh
3548                                        ) xyz
3549                                    ]
3550                                  : let( // Heightfield texture
3551                                        texcnt = [len(texture[0]), len(texture)]
3552                                    ) [
3553                                        for (i = [0:1:counts_y-(closed?1:0)], ti = [0:1:texcnt.y-1])
3554                                        if (i != counts_y || ti == 0)
3555                                        let(
3556                                            part = (i + (ti/texcnt.y)) * samples,
3557                                            u = floor(part),
3558                                            uu = part - u,
3559                                            base = lerp(bases[u], select(bases,u+1), uu),
3560                                            norm = unit(lerp(norms[u], select(norms,u+1), uu)),
3561                                            tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
3562                                            texh = (texture[ti][0] - inset) * tex_scale * (base.x / maxx),
3563                                            xyz = base - norm * texh
3564                                        ) xyz
3565                                    ],
3566                                path = closed? ppath : [
3567                                    [0, ppath[0].y],
3568                                    each ppath,
3569                                    [0, last(ppath).y],
3570                                ]
3571                            ) deduplicate(path, closed=closed)
3572                        ],
3573                        vnf2 = vnf_from_region(cap_rgn, xrot(90), reverse=false),
3574                        vnf3 = vnf_from_region(cap_rgn, rot([90,0,angle]), reverse=true)
3575                    ) vnf_join([vnf2, vnf3]),
3576                allcaps_vnf = closed? EMPTY_VNF :
3577                    let(
3578                        plen = path_length(rgn[0], closed=closed),
3579                        counts_y = is_vector(counts,2)? counts.y :
3580                            is_vector(tex_size,2)? max(1,round(plen/tex_size.y)) : 6,
3581                        obases = resample_path(rgn[0], n=counts_y * samples + (closed?0:1), closed=closed),
3582                        onorms = path_normals(obases, closed=closed),
3583                        rbases = closed? close_path(obases) : obases,
3584                        rnorms = closed? close_path(onorms) : onorms,
3585                        bases = xrot(90, p=path3d(rbases)),
3586                        norms = xrot(90, p=path3d(rnorms)),
3587                        caps_vnf = vnf_join([
3588                            for (j = [-1,0]) let(
3589                                base = select(bases,j),
3590                                norm = unit(select(norms,j)),
3591                                ppath = [
3592                                    for (vert = bpath) let(
3593                                        uang = vert.x / counts_x,
3594                                        tex_scale = tex_scale * lookup([0,1][j+1], taper_lup),
3595                                        texh = (vert.y - inset) * tex_scale * (base.x / maxx),
3596                                        xyz = base - norm * texh
3597                                    ) zrot(angle*uang, p=xyz)
3598                                ],
3599                                pplen = len(ppath),
3600                                zed = j<0? max(column(ppath,2)) :
3601                                    min(column(ppath,2)),
3602                                slice_vnf = [
3603                                    [
3604                                        each ppath,
3605                                        [0, 0, zed],
3606                                    ], [
3607                                        for (i = [0:1:pplen-2])
3608                                            j<0? [pplen, i, (i+1)%pplen] :
3609                                            [pplen, (i+1)%pplen, i]
3610                                    ]
3611                                ],
3612                                cap_vnf = vnf_join([
3613                                    for (i = [0:1:counts_x-1])
3614                                        zrot(i*angle/counts_x, p=slice_vnf)
3615                                ])
3616                            ) cap_vnf
3617                        ])
3618                    ) caps_vnf
3619            ) vnf_join([walls_vnf, endcap_vnf, allcaps_vnf])
3620        ]),
3621        skmat = down(-miny) * skew(sxz=shift.x/h, syz=shift.y/h) * up(-miny),
3622        skvnf = apply(skmat, full_vnf),
3623        geom = atype=="intersect"
3624              ? attach_geom(vnf=skvnf, extent=false)
3625              : attach_geom(vnf=skvnf, extent=true)
3626    ) reorient(anchor,spin,orient, geom=geom, p=skvnf);
3627
3628
3629module _textured_revolution(
3630    shape, texture, tex_size, tex_scale=1,
3631    inset=false, rot=false, shift=[0,0],
3632    taper, closed=true, angle=360,
3633    style="min_edge", atype="intersect",
3634    convexity=10, counts, samples,
3635    anchor=CENTER, spin=0, orient=UP
3636) {
3637    dummy = assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"");
3638    vnf = _textured_revolution(
3639        shape, texture, tex_size=tex_size,
3640        tex_scale=tex_scale, inset=inset, rot=rot,
3641        taper=taper, closed=closed, style=style,
3642        shift=shift, angle=angle,
3643        samples=samples, counts=counts
3644    );
3645    geom = atype=="intersect"
3646          ? attach_geom(vnf=vnf, extent=false)
3647          : attach_geom(vnf=vnf, extent=true);
3648    attachable(anchor,spin,orient, geom=geom) {
3649        vnf_polyhedron(vnf, convexity=convexity);
3650        children();
3651    }
3652}
3653
3654
3655
3656// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap