texture support for vnf_vertex_array

This commit is contained in:
Adrian Mariano
2025-04-10 22:12:16 -04:00
parent f7a5eeaa11
commit b18a8330bd
3 changed files with 282 additions and 33 deletions

View File

@@ -819,7 +819,8 @@ function prismoid(
// You can specify the size of the ends using diameter or radius measured either inside or outside. Alternatively
// you can give the length of the side of the polygon. You can specify chamfers and roundings for the ends, but not
// the vertical edges. See {{rounded_prism()}} for prisms with rounded vertical edges. You can also specify texture for the side
// faces, but note that texture is not compatible with any roundings or chamfers.
// faces, but note that texture is not compatible with any roundings or chamfers.
// See [Texturing](skin.scad#section-texturing) for more details on how textures work.
// .
// Anchors are based on the VNF of the prism. Especially for tapered or shifted prisms, this may give unexpected anchor positions, such as top side anchors
// being located at the bottom of the shape, so confirm anchor positions before use.
@@ -1158,7 +1159,9 @@ function regular_prism(n,
// textured_tile(texture, [size], [w1=], [w2=], [ang=], [shift=], [h=/height=/thickness=], [atype=], [diff=], [tex_extra=], [tex_skip=], ...) [ATTACHMENTS];
// vnf = textured_tile(texture, [size], [w1=], [w2=], [ang=], [shift=], [h=/height=/thickness=], [atype=], [tex_extra=], [tex_skip=], ...);
// Description:
// Creates a cuboid or trapezoidal prism and places a texture on the top face. You can specify the size by giving a `size` scalar or vector as is
// Creates a cuboid or trapezoidal prism and places a texture on the top face.
// See [Texturing](skin.scad#section-texturing) for more details on how textures work.
// You can specify the size of the object by giving a `size` scalar or vector as is
// usual for a cube. If you give a scalar, however, it applies only to the X and Y dimensions: the default is to create a thin tile, not a cube.
// The Z size specifies the size of the shape **not** including the applied texture (in the same way that other textured objects work).
// If you omit the Z value then for regular textures, the default thickness will be 0.1 which provides a thin backing layer. A zero thickness
@@ -1373,14 +1376,9 @@ function textured_tile(
height = is_def(size) ? default(size.z,default_thick) : one_defined([h,height,thickness],"h,height,thickness",dflt=default_thick),
size = is_def(size) ? is_num(size) ? [size,size,1] : point3d(size,1) // We only use the x and y components of size
: [w1,ysize],
tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture,
texture = tex_rot==0? tex
: is_vnf(tex)? zrot(tex_rot, cp=[1/2,1/2], p=tex)
: tex_rot==180? reverse([for (row=tex) reverse(row)])
: tex_rot==270? [for (row=transpose(tex)) reverse(row)]
: reverse(transpose(tex)),
check_tex = _validate_texture(texture),
texture = _get_texture(texture, tex_rot),
tex_reps = is_def(tex_reps) ? tex_reps
: [round(size.x/tex_size.x), round(size.y/tex_size.y)],
scale = [size.x/tex_reps.x, size.y/tex_reps.y],
@@ -1989,8 +1987,10 @@ function cylinder(h, r1, r2, center, r, d, d1, d2, anchor, spin=0, orient=UP) =
// the cylinder or cone's sloped side. The more specific parameters like chamfer1 or rounding2 override the more
// general ones like chamfer or rounding, so if you specify `rounding=3, chamfer2=3` you will get a chamfer at the top and
// rounding at the bottom. You can specify extra height at either end for use with difference(); the extra height is ignored by
// anchoring.
// anchoring.
// .
// You can apply a texture to the cylinder using the usual texture parameters.
// See [Texturing](skin.scad#section-texturing) for more details on how textures work.
// When creating a textured cylinder, the number of facets is determined by the sampling of the texture. Any `$fn`, `$fa` or `$fs` values in
// effect are ignored. To create a textured prism with a specified number of flat facets use {{regular_prism()}}. Anchors for cylinders
// appear on the ideal cylinder, not on actual discretized shape the module produces. For anchors on the shape surface, use {{regular_prism()}}.

243
skin.scad
View File

@@ -3150,7 +3150,7 @@ function associate_vertices(polygons, split, curpoly=0) =
// DefineHeader(Table;Headers=Texture Name|Type|Description): Texture Values
// Section: Texturing
// Some operations are able to add texture to the objects they create. A texture can be any regularly repeated variation in the height of the surface.
@@ -4036,6 +4036,29 @@ function _validate_texture(texture) =
true;
function _tex_height(scale, inset, z) = scale<0 ? -(1-z - inset) * scale
: (z - inset) * scale;
function _get_texture(texture, tex_rot, extra_rot=0) =
let(
tex_rot=!is_bool(tex_rot)? tex_rot
: echo("boolean value for tex_rot is deprecated. Use a numerical angle divisible by 90.") tex_rot?90:0
)
assert(is_num(tex_rot) && posmod(tex_rot,90)==0, "tex_rot must be a multiple of 90 degrees")
let(
tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture,
check_tex = _validate_texture(tex),
tex_rot = posmod(tex_rot+extra_rot,360),
)
tex_rot==0 ? tex
: is_vnf(tex)? zrot(tex_rot, cp=[1/2,1/2], p=tex)
: tex_rot==180? reverse([for (row=tex) reverse(row)])
: tex_rot==270? [for (row=transpose(tex)) reverse(row)]
: reverse(transpose(tex));
function _textured_linear_sweep(
region, texture, tex_size=[5,5],
h, counts, inset=false, rot=0,
@@ -4065,14 +4088,8 @@ function _textured_linear_sweep(
caps = is_bool(caps) ? [caps,caps] : caps,
regions = is_path(region,2)? [[region]] : region_parts(region),
tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture,
dummy = assert(is_undef(samples) || is_vnf(tex), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"),
dummy2=is_bool(rot)?echo("boolean value for tex_rot is deprecated. Use a numerical angle, one of 0, 90, 180, or 270.")0:0,
texture = !rot? tex :
is_vnf(tex)? zrot(is_num(rot)?rot:90, cp=[1/2,1/2], p=tex) :
rot==180? reverse([for (row=tex) reverse(row)]) :
rot==270? [for (row=transpose(tex)) reverse(row)] :
reverse(transpose(tex)),
texture = _get_texture(texture, tex_rot),
dummy = assert(is_undef(samples) || is_vnf(texture), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"),
h = first_defined([h, l, height, length, 1]),
inset = is_num(inset)? inset : inset? 1 : 0,
twist = default(twist, 0),
@@ -4081,7 +4098,6 @@ function _textured_linear_sweep(
is_num(scale)? [scale,scale,1] : scale,
samples = !is_vnf(texture)? len(texture[0]) :
is_num(samples)? samples : 8,
check_tex = _validate_texture(texture),
vnf_tile =
!is_vnf(texture) || samples==1 ? texture
:
@@ -4352,15 +4368,8 @@ function _textured_revolution(
)
assert(closed || is_path(shape,2))
let(
tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture,
texture = _get_texture(texture, tex_rot),
dummy = assert(is_undef(samples) || is_vnf(tex), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"),
dummy2=is_bool(rot)?echo("boolean value for tex_rot is deprecated. Use a numerical angle, one of 0, 90, 180, or 270.")0:0,
texture = !rot? tex :
is_vnf(tex)? zrot(is_num(rot)?rot:90, cp=[1/2,1/2], p=tex) :
rot==180? reverse([for (row=tex) reverse(row)]) :
rot==270? [for (row=transpose(tex)) reverse(row)] :
reverse(transpose(tex)),
check_tex = _validate_texture(texture),
inset = is_num(inset)? inset : inset? 1 : 0,
samples = !is_vnf(texture)? len(texture) :
is_num(samples)? samples : 8,
@@ -4620,5 +4629,203 @@ module _textured_revolution(
}
function _texture_point_array(points, texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, triangulate=false,
col_wrap=false, tex_depth=1, row_wrap=false, caps, cap1, cap2, reverse=false, style="min_edge", tex_extra, tex_skip, sidecaps,sidecap1,sidecap2) =
assert(tex_reps==undef || is_vector(tex_reps,2))
assert(tex_size==undef || is_num(tex_size) || is_vector(tex_size,2), "tex_size must be a scalar or 2-vector")
assert(num_defined([tex_size, tex_reps])<2, "Cannot give both tex_size and tex_reps")
assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2"]))
assert(is_matrix(points[0], n=3),"Point array has the wrong shape or points are not 3d")
assert(is_consistent(points), "Non-rectangular or invalid point array")
let(
cap1 = first_defined([cap1,caps,false]),
cap2 = first_defined([cap2,caps,false]),
sidecap1 = first_defined([sidecap1,sidecaps,false]),
sidecap2 = first_defined([sidecap2,sidecaps,false]),
tex_inset = is_num(tex_inset)? tex_inset : tex_inset? 1 : 0,
texture = _get_texture(texture, tex_rot),
dummy = assert(is_undef(tex_samples) || is_vnf(texture),
"You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"),
ptsize=[len(points[0]), len(points)],
tex_reps = is_def(tex_reps) ? tex_reps
: let(
tex_size = is_undef(tex_sizes) ? [5,5] : force_list(tex_size,2),
xsize = norm(points[0][0]-points[0][1])*(ptsize.x+(col_wrap?1:0)),
ysize = norm(points[0][0]-points[1][0])*(ptsize.y+(row_wrap?1:0))
)
[round(xsize/tex_size.x), round(ysize/tex_size.y)],
normals = surfnormals(points, col_wrap=col_wrap, row_wrap=row_wrap),
getscale = function(x,y) (x+y)/2
)
!is_vnf(texture) ? // heightmap case
let(
extra = is_def(tex_extra) ? force_list(tex_extra,2)
: [col_wrap?0:1, row_wrap?0:1],
skip = is_def(tex_skip) ? force_list(tex_skip,2) : [0,0],
texsize = [len(texture[0]), len(texture)],
fullsize = [texsize.x*tex_reps.x+extra.x-skip.x, texsize.y*tex_reps.y+extra.y-skip.y],
res_points = resample(points,fullsize, col_wrap=col_wrap, row_wrap=row_wrap),
res_normals=resample(normals,fullsize, col_wrap=col_wrap, row_wrap=row_wrap),
local_scale = [for(y=[0:1:fullsize.y-1])
[for(x=[0:1:fullsize.x-1])
let(
xlen = [
if(x>0 || col_wrap) norm(res_points[y][x] - select(res_points[y], x-1)),
if(x<fullsize.x-1 || col_wrap) norm(res_points[y][x] - select(res_points[y], x+1))
],
ylen = [
if(y>0 || row_wrap) norm(res_points[y][x] - select(res_points,y-1)[x]),
if(y<fullsize.y-1 || row_wrap) norm(res_points[y][x] - select(res_points,y+1)[x])
]
)
getscale(mean(xlen),mean(ylen))
]
],
tex_surf =
[for(y=[0:1:fullsize.y-1])
[for(x=[0:1:fullsize.x-1])
let(yind = (y+skip.y)%texsize.y,
xind = (x+skip.x)%texsize.x,
)
res_points[y][x] + _tex_height(tex_depth,tex_inset,texture[yind][xind]) * res_normals[y][x]*(reverse?-1:1)*local_scale[y][x]/local_scale[0][0]
]
]
)
vnf_vertex_array(tex_surf, row_wrap=row_wrap, col_wrap=col_wrap, reverse=reverse,style=style, caps=caps, triangulate=triangulate)
: // VNF case
let(
local_scale = [for(y=[-1:1:ptsize.y-1])
[for(x=[-1:1:ptsize.x-1])
((!col_wrap && (x<0 || x==ptsize.x-1))
|| (!row_wrap && (y<0 || y==ptsize.y-1))) ? undef
: let(
dx = [norm(select(select(points,y),x) - select(select(points,y),x+1)),
norm(select(select(points,y+1),x) - select(select(points,y+1),x+1))],
dy = [norm(select(select(points,y),x) - select(select(points,y+1),x)),
norm(select(select(points,y),x+1) - select(select(points,y+1),x+1))]
)
getscale(mean(dx),mean(dy))]],
samples = default(tex_samples,8),
vnf = samples==1? texture :
let(
s = 1 / samples,
slice_us = list([s:s:1-s/2]),
vnft1 = vnf_slice(texture, "X", slice_us),
vnft = vnf_slice(vnft1, "Y", slice_us),
zvnf = [
[
for (p=vnft[0]) [
approx(p.x,0)? 0 : approx(p.x,1)? 1 : p.x,
approx(p.y,0)? 0 : approx(p.y,1)? 1 : p.y,
p.z
]
],
vnft[1]
]
)
zvnf,
yedge_paths = !row_wrap ? _tile_edge_path_list(vnf,1) : undef,
xedge_paths = !col_wrap ? _tile_edge_path_list(vnf,0) : undef,
trans_pt = function(x,y,pt)
let(
tileindx = x+pt.x,
tileindy = y+(1-pt.y),
refx = tileindx/tex_reps.x*(ptsize.x-(col_wrap?0:1)),
refy = tileindy/tex_reps.y*(ptsize.y-(row_wrap?0:1)),
xind = floor(refx),
yind = floor(refy),
xfrac = refx-xind,
yfrac = refy-yind,
corners = [points[yind%ptsize.y][xind%ptsize.x], points[(yind+1)%ptsize.y][xind%ptsize.x],
points[yind%ptsize.y][(xind+1)%ptsize.x], points[(yind+1)%ptsize.y][(xind+1)%ptsize.x]],
base = bilerp(corners,xfrac, yfrac),
scale_list = xfrac==0 && yfrac==0 ? [local_scale[yind][xind], local_scale[yind][xind+1], local_scale[yind+1][xind], local_scale[yind+1][xind+1]]
: xfrac==0 ? [local_scale[yind+1][xind], local_scale[yind+1][xind+1]]
: yfrac==0 ? [local_scale[yind][xind+1], local_scale[yind+1][xind+1]]
: [ local_scale[yind+1][xind+1]],
scale = mean([for(s=scale_list) if (is_def(s)) s])/local_scale[1][1],
normal = bilerp([normals[yind%ptsize.y][xind%ptsize.x], normals[(yind+1)%ptsize.y][xind%ptsize.x],
normals[yind%ptsize.y][(xind+1)%ptsize.x], normals[(yind+1)%ptsize.y][(xind+1)%ptsize.x]],
xfrac, yfrac)
)
base + _tex_height(tex_depth,tex_inset,pt.z) * normal*(reverse?-1:1) * scale,
fullvnf = vnf_join([
for(y=[0:1:tex_reps.y-1], x=[0:1:tex_reps.x-1])
[
[for(pt=vnf[0]) trans_pt(x,y,pt)],
vnf[1]
],
for(y=[if (cap1) 0, if (cap2) tex_reps.y-1])
let(
cap_paths = [
if (col_wrap && len(yedge_paths[0])>0)
[for(x=[0:1:tex_reps.x-1], pt=yedge_paths[0][0])
trans_pt(x,y,[pt.x,y?0:1,pt.z])],
if (!row_wrap)
for(closed_path=yedge_paths[1], x=[0:1:tex_reps.x-1])
[for(pt = closed_path) trans_pt(x,y,[pt.x,y?0:1,pt.z])]
]
)
for(path=cap_paths) [path, [count(path,reverse=y==0)]],
if (!col_wrap)
for(x=[if (sidecap1) 0, if (sidecap2) tex_reps.x-1])
let(
cap_paths = [for(closed_path=xedge_paths[1], y=[0:1:tex_reps.y-1])
[for(pt = closed_path) trans_pt(x,y,[x?1:0,pt.y,pt.z])]]
)
for(path=cap_paths) [path, [count(path,reverse=x!=0)]]
])
)
reverse ? vnf_reverse_faces(fullvnf) : fullvnf;
///// These need to be either hidden or documented and placed somewhere.
function bilerp(pts,x,y) =
[1,x,y,x*y]*[[1, 0, 0, 0],[-1, 0, 1, 0],[-1,1,0,0],[1,-1,-1,1]]*pts;
function resample(data, size, col_wrap=false, row_wrap=false) =
let(
xL=len(data[0]),
yL=len(data),
lastx=xL-(col_wrap?0:1),
lasty=yL-(row_wrap?0:1),
lastoutx = size.x - (col_wrap?0:1),
lastouty = size.y - (row_wrap?0:1),
xscale = lastx/lastoutx,
yscale = lasty/lastouty
)
[
for(y=[0:1:lastouty])
[
for(x=[0:1:lastoutx])
let(
sx = xscale*x,
sy = yscale*y,
xind=floor(sx),
yind=floor(sy)
)
bilerp([data[yind%yL][xind%xL], data[(yind+1)%yL][xind%xL],
data[yind%yL][(xind+1)%xL], data[(yind+1)%yL][(xind+1)%xL]],
sx-xind, sy-yind)
]
];
function surfnormals(data, col_wrap=false, row_wrap=false) =
let(
rowderivs = [for(y=[0:1:len(data)-1]) path_tangents(data[y],closed=col_wrap)],
colderivs = [for(x=[0:1:len(data[0])-1]) path_tangents(column(data,x), closed=row_wrap)]
)
[for(y=[0:1:len(data)-1])
[for(x=[0:1:len(data[0])-1])
cross(colderivs[x][y],rowderivs[y][x])]];
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap

View File

@@ -48,6 +48,23 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces.
// adds a vertex in the center of each quadrilateral and creates four triangles, and the "convex" and "concave" styles
// choose the locally convex/concave subdivision. The "min_area" option creates the triangulation with the minimal area. Degenerate faces
// are not included in the output, but if this results in unused vertices they still appear in the output.
// .
// You can apply a texture to the vertex array VNF using the usual texture parameters.
// See [Texturing](skin.scad#section-texturing) for more details on how textures work.
// The top left corner of the texture tile will be aligned with `points[0][0]`, and the the X and Y directions correspond to `points[y][x]`.
// In practice, it is probably easiest to observe the result and apply a suitable texture tile rotation by setting `tex_rot` if the result
// is not what you wanted. The reference scale of your point data is also taken from the square at the [0][0] corner. This determines
// the meaning of `tex_size` and it also affects the vertical texture scale. The size of the texture tiles will be proportional to the point
// spacing of the location where they are placed, so if the points are closer together, you will get small texture elements. The `tex_depth` you
// specify will be correct at the `points[0][0]` but will be different at places in the point array where the scale is different. Note that this
// differs from {{rotate_sweep()}} which uses a uniform resampling of the curve you specify.
// .
// The point data for `vnf_vertex_array()` is resampled using bilinear interpolation to match the required point density of the tile count, but the
// sampling is based on the grid, not on the distance between points. If you want to
// avoid resampling, match the point data to the required point number for your tile count. For height field textures this means
// the number of data points must equal the tile count times the number of entries in the tile minus `tex_skip` plus `tex_extra`.
// Note that `tex_extra` defaults to 1 along dimensions that are not wrapped. For a VNF tile you need to have the the point
// count equal to the tile count times tex_samples, plus one if wrapping is disabled.
// Arguments:
// points = A list of vertices to divide into columns and rows.
// ---
@@ -60,6 +77,18 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces.
// style = The style of subdividing the quads into faces. Valid options are "default", "alt", "flip1", "flip2", "min_edge", "min_area", "quincunx", "convex" and "concave".
// triangulate = If true, triangulates endcaps to resolve possible CGAL issues. This can be an expensive operation if the endcaps are complex. Default: false
// convexity = (module) Max number of times a line could intersect a wall of the shape.
// 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.
// tex_size = An optional 2D target size for the textures at `points[0][0]`. Actual texture sizes will be scaled somewhat to evenly fit the available surface. Default: `[5,5]`
// tex_reps = If given instead of tex_size, a 2-vector giving the number of texture tile repetitions in the horizontal and vertical directions.
// tex_inset = If numeric, lowers the texture into the surface by the specified proportion, e.g. 0.5 would lower it half way into the surface. If `true`, insets by exactly its full depth. Default: `false`
// tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0
// tex_depth = Specify texture depth; if negative, invert the texture. Default: 1.
// tex_samples = Minimum number of "bend points" to have in VNF texture tiles. Default: 8
// tex_extra = number of extra lines of a hightfield texture to add at the end. Can be a scalar or 2-vector to give x and y values. Default: 1
// tex_skip = number of lines of a heightfield texture to skip when starting. Can be a scalar or two vector to give x and y values. Default: 0
// sidecaps = if `col_wrap==false` this controls whether to cap any floating ends of a VNF tile on the texture. Does not affect the main texture surface. Ignored it doesn't apply. Default: false
// sidecap1 = set sidecap only for the `points[][0]` edge of the output
// sidecap2 = set sidecap only for the `points[][max]` edge of the output
// cp = (module) 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"
// anchor = (module) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
// spin = (module) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
@@ -181,10 +210,15 @@ module vnf_vertex_array(
reverse=false,
style="default",
triangulate = false,
texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0,
tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2,
convexity=2, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull")
{
vnf = vnf_vertex_array(points=points, caps=caps, cap1=cap2, cap2=cap2,
col_wrap=col_wrap, row_wrap=row_wrap, reverse=reverse, style=style,triangulate=triangulate);
col_wrap=col_wrap, row_wrap=row_wrap, reverse=reverse, style=style,triangulate=triangulate,
texture=texture, tex_reps=tex_reps, tex_size=tex_size, tex_samples=tex_samples, tex_inset=tex_inset, tex_rot=tex_rot,
tex_depth=tex_depth, tex_extra=tex_extra, tex_skip=tex_skip, sidecaps=sidecaps,sidecap1=sidecap1,sidecap2=sidecap2
);
vnf_polyhedron(vnf, convexity=2, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull") children();
}
@@ -196,14 +230,22 @@ function vnf_vertex_array(
row_wrap=false,
reverse=false,
style="default",
triangulate = false
triangulate = false,
texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0,
tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2
) =
assert(!(any([caps,cap1,cap2]) && !col_wrap), "col_wrap must be true if caps are requested")
assert(!(any([caps,cap1,cap2]) && row_wrap), "Cannot combine caps with row_wrap")
assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2"]))
assert(is_matrix(points[0], n=3),"Point array has the wrong shape or points are not 3d")
assert(is_consistent(points), "Non-rectangular or invalid point array")
assert(is_bool(triangulate))
is_def(texture) ?
_texture_point_array(points=points, texture=texture, tex_reps=tex_reps, tex_size=tex_size,
tex_inset=tex_inset, tex_samples=tex_samples, tex_rot=tex_rot,
col_wrap=col_wrap, row_wrap=row_wrap, tex_depth=tex_depth, caps=caps, cap1=cap1, cap2=cap2, reverse=reverse,
style=style, tex_extra=tex_extra, tex_skip=tex_skip, sidecaps=sidecaps, sidecap1=sidecap1, sidecap2=sidecap2,triangulate=triangulate)
:
assert(!(any([caps,cap1,cap2]) && !col_wrap), "col_wrap must be true if caps are requested (without texture)")
assert(!(any([caps,cap1,cap2]) && row_wrap), "Cannot combine caps with row_wrap (without texture)")
let(
pts = flatten(points),
pcnt = len(pts),