diff --git a/drawing.scad b/drawing.scad index 1ecaaedc..23f3042b 100644 --- a/drawing.scad +++ b/drawing.scad @@ -670,8 +670,8 @@ module dashed_stroke(path, dashpat=[3,3], width=1, closed=false, fit=true, round // Function&Module: arc() // Synopsis: Draws a 2D pie-slice or returns 2D or 3D path forming an arc. -// SynTags: Geom, Path -// Topics: Paths (2D), Paths (3D), Shapes (2D), Path Generators +// SynTags: Geom, Path +// Topics: Paths (2D), Paths (3D), Shapes (2D), Path Generators, Rounding // See Also: pie_slice(), stroke(), ring() // // Usage: 2D arc from 0ยบ to `angle` degrees. @@ -689,7 +689,7 @@ module dashed_stroke(path, dashpat=[3,3], width=1, closed=false, fit=true, round // Usage: 2D or 3D arc, fron tangent point on segment `[P0,P1]` to the tangent point on segment `[P1,P2]`. // path=arc(n, corner=[P0,P1,P2], r=); // Usage: Create a wedge using any other arc parameters -// path=arc(wedge=true,...) +// path=arc(wedge=true,[rounding=],...) // Usage: as module // arc(...) [ATTACHMENTS]; // Description: @@ -744,10 +744,14 @@ module dashed_stroke(path, dashpat=[3,3], width=1, closed=false, fit=true, round // path = arc(corner=pts, r=20); // stroke(pts, endcaps="arrow2"); // stroke(path, endcap2="arrow2", color="blue"); -// Example(2D): Rounding the corners +// Example(2D, NoScales): Rounding the corners // $fs=.5; $fa=1; // arc(r=25, angle=[25,107], rounding=[6,5,7], wedge=true); // stroke(arc(r=25, angle=[25,107], wedge=true), color="red",closed=true, width=.5); +// Example(2D, NoScales): Negative roundings are permitted on the two outside corners, but not the center corner. +// $fs=.5; $fa=1; +// arc(r=25, angle=[-30,45], rounding=[0,-12, -27], wedge=true); +// stroke(arc(r=25, angle=[-30,45], wedge=true), color="red",closed=true, width=.5); function arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=false, long=false, cw=false, ccw=false, endpoint=true, rounding) = assert(is_bool(endpoint)) @@ -892,33 +896,38 @@ module arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=fa } + + function _rounded_arc(radius, rounding=0, angle, n) = - assert(is_finite(angle) && angle>-360 && angle<360, "angle must be strictly between -360 and 360") + assert(is_finite(angle) && abs(angle)<360, "angle must be strictly between -360 and 360") assert(is_finite(rounding) || is_vector(rounding,3), "rounding must be a scalar or 3-vector") - assert(all_nonnegative(rounding), "rounding values must be nonnegative") let( rounding = force_list(rounding,3), + dir = sign(angle), -// inner_corner_radius = abs(angle)==180?0 : abs(angle)>180 ? -dir*rounding[0] : dir*rounding[0], inner_corner_radius = abs(angle)>180 ? -dir*rounding[0] : dir*rounding[0], arc1_opt_radius = radius - rounding[1], arc2_opt_radius = radius - rounding[2], - check = assert(rounding[1]=0, "rounding[0] must be nonnegative") + assert(rounding[1]0 ? [-dir*90, dir*arc1_angle] : -[dir*90, dir*180 - arc1_angle], + angle_span2 = [angle-dir*arc2_angle + (rounding[2]<0 ? dir*180 : 0), angle+dir*90] ) assert(arc1_angle + arc2_angle<=abs(angle), "Roundings are too large: they interfere with each other on the arc") assert(edge_gap1>=0, "Roundings are too large: center rounding (rounding[0]) interferes with first corner (rounding[1])") @@ -926,14 +935,14 @@ function _rounded_arc(radius, rounding=0, angle, n) = [ each if (rounding[0]>0 && abs(angle)!=180) arc(cp=pt2, - points=[polar_to_xy(r=radius_of_ctrpt_edge, theta=angle), // origin corner curve + points=[polar_to_xy(r=radius_of_ctrpt_edge, theta=angle), // origin corner curve polar_to_xy(r=radius_of_ctrpt_edge, theta=0)], endpoint=edge_gap1!=0,n=n) else repeat([0,0],rounding[0]>0 && abs(angle)==180 && is_def(n) ? n : 1), - each if (rounding[1]>0) arc(r=rounding[1],cp=pt1,angle=[-90*dir,dir*arc1_angle],endpoint=dir*arc1_angle==angle,n=n), // first corner + each if (rounding[1]!=0) arc(r=abs(rounding[1]),cp=pt1,angle=angle_span1,endpoint=dir*arc1_angle==angle,n=n), // first corner each if (arc1_angle+arc2_angle0) arc(r=rounding[2],cp=pt3, angle=[angle-dir*arc2_angle, angle+dir*90],endpoint=edge_gap2!=0,n=n), // second corner + arc(r=radius, angle=[dir*arc1_angle,angle - dir*arc2_angle], endpoint=rounding[2]==0, n=n), // main arc section + each if (rounding[2]!=0) arc(r=abs(rounding[2]),cp=pt3, angle=angle_span2, endpoint=edge_gap2!=0, n=n) // second corner ]; diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index bcfc4944..ce07beb3 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -10,6 +10,16 @@ if (( ${#INFILES[@]} == 0 )); then INFILES=(tests/test_*.scad) fi + +cleanup () { + rm -f out.echo + exit +} + +# clean up out.echo if we terminate due to a signal + +trap cleanup SIGINT SIGHUP SIGQUIT SIGABRT + OUTCODE=0 for testfile in "${INFILES[@]}"; do if [[ -f "$testfile" ]] ; then diff --git a/shapes3d.scad b/shapes3d.scad index 0f1c6f87..e4cbd59d 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -1226,7 +1226,7 @@ function regular_prism(n, // Example(3D,NoAxes,VPT=[-0.0852782,0.259593,0.139667],VPR=[58.5,0,345.1],VPD=36.0994): Attaching a tile to a cube // cuboid([12,12,4]) attach(TOP,BOT) // textured_tile("trunc_pyramids", 10, tex_reps=[5,5], style="convex"); -// Example(3D,NoScales,VPT = [-0.0788193, 0.10015, -0.0938629], VPR = [57.8, 0, 34.1], VPD = 29.2405): This inset texture doesn't look obviously different, but you can see that the object is below the XY plane. +// Example(3D,NoScales,VPT = [-0.0788193, 0.10015, -0.0938629], VPR = [57.8, 0, 34.1], VPD = 29.2405): This inset texture doesn't look obviously different, but you can see that the object is below the XY plane. // textured_tile("trunc_pyramids_vnf", 10, tex_reps=[5,5], tex_inset=true); // Example(3D,NoAxes,VPT=[0.242444,0.170054,-0.0714754],VPR=[67.6,0,33.4],VPD=36.0994): Here we use the `diff` option combined with {{diff()}} to attach the inset texture to the front of a parent cuboid. // diff() @@ -1250,36 +1250,6 @@ function regular_prism(n, // textured_tile("trunc_ribs", 10, tex_reps=[5,2], tex_skip=1); -// This is like _tile_edge_path_list in that it finds the paths -// on the x=0 or y=0 face of a VNF tile. But instead of returning -// paths it turns them into a VNF. The open path is closed to the specified -// z coordinate. - -function _tile_edge_vnf(vnf, axis, z, maxopen=1) = - let( - verts = vnf[0], - faces = vnf[1], - segs = [for(face=faces, edge=pair(select(verts,face),wrap=true)) if (approx(edge[0][axis],0) && approx(edge[1][axis],0)) [edge[1],edge[0]]], - paths = _assemble_partial_paths(segs), - facelist = [ - for(path=paths) - if (!(len(path)<=3 && path[0]==last(path))) - path[0]==len(path) ? [list_unwrap(path),[count(len(path)-1)]] - : [ - [ - point3d(point2d(path[0]),z), - each path, - point3d(point2d(last(path)),z) - ], - [count(len(path)+2)] - ] - ], - openlist = [for(entry=facelist) if (!are_ends_equal(entry[0])) 1] - ) - assert(len(openlist)<=maxopen, str("VNF has ",len(openlist)," open paths on an edge and at most ",maxopen," is supported.")) - vnf_join(facelist); - - module textured_tile( texture, size, @@ -1382,6 +1352,7 @@ function textured_tile( 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], + setz=function (v,z) [v.x,v.y,z], vnf = !is_vnf(texture) ? let( texsteps = [len(texture[0]), len(texture)], @@ -1412,18 +1383,57 @@ function textured_tile( ], scaled_vnf = scale(scale, zadj_vnf), tiled_vnf = [for(i=[0:1:tex_reps.x-1], j=[0:1:tex_reps.y-1]) move([scale.x*i,scale.y*j], scaled_vnf)], - unscaled_hedge=_tile_edge_vnf(zadj_vnf,1,-height/2), - hedge = unscaled_hedge==EMPTY_VNF ? [] : xscale(scale.x, unscaled_hedge), - unscaled_vedge=_tile_edge_vnf(zadj_vnf,0,-height/2), - vedge = unscaled_vedge==EMPTY_VNF ? [] : yscale(scale.y, unscaled_vedge), - hedge_flip = hedge==[] ? hedge : back(size.y,vnf_reverse_faces(hedge)), - vedge_flip = vedge==[] ? vedge : right(size.x,vnf_reverse_faces(vedge)), - front_edge = hedge==[] ? [] : [for(i=[0:1:tex_reps.x-1]) xmove(scale.x*i, hedge)], - left_edge = vedge==[] ? [] : [for(j=[0:1:tex_reps.y-1]) ymove(scale.y*j, vedge)], - back_edge = hedge==[] ? [] : [for(i=[0:1:tex_reps.x-1]) xmove(scale.x*i, hedge_flip)], - right_edge = vedge==[] ? [] : [for(j=[0:1:tex_reps.y-1]) ymove(scale.y*j, vedge_flip)], + + yedge_list = _tile_edge_path_list(zadj_vnf, 0), + xedge_list = _tile_edge_path_list(zadj_vnf, 1), + + front_back_closed = [for(i=[0:1:tex_reps.x-1], cpath=xedge_list[1]) + each [[xscale(scale.x,xmove(i,cpath)), [count(cpath)]], + [xscale(scale.x,move([i,size.y],cpath)),[count(cpath,reverse=true)]]]], + sides_closed = [for(j=[0:1:tex_reps.y-1], cpath=yedge_list[1]) + each [[yscale(scale.y,ymove(j,cpath)), [count(cpath)]], + [yscale(scale.y,move([size.x, j], cpath)),[count(cpath,reverse=true)]]]], + + leftpath = yedge_list[0]==[] ? [] + : deduplicate([for(j=[0:1:tex_reps.y-1]) each reverse(yscale(scale.y,ymove(j,yedge_list[0][0])))]), + frontpath = xedge_list[0]==[] ? [] + : deduplicate([for(i=[0:1:tex_reps.x-1]) each xscale(scale.x,xmove(i,xedge_list[0][0]))]), + + base = frontpath==[] || leftpath==[] ? [] + : [ + [ + [setz(frontpath[0],-height/2), + each frontpath, + setz(last(frontpath), -height/2) + ], + [count(len(frontpath)+2)] + ], + [ + [setz(last(leftpath),-height/2), + each reverse(leftpath), + setz(leftpath[0], -height/2) + ], + [count(len(leftpath)+2)] + ], + [ + back(size.y, + [setz(last(frontpath),-height/2), + each reverse(frontpath), + setz(frontpath[0],-height/2) + ]), + [count(len(frontpath)+2)] + ], + [right(size.x, + [setz(leftpath[0],-height/2), + each leftpath, + setz(last(leftpath),-height/2) + ]), + [count(len(leftpath)+2)] + ] + ], + bottom = [path3d(rect(point2d(size),anchor=FWD+LEFT),-height/2), [[3,2,1,0]]], - result = vnf_join(concat(tiled_vnf, front_edge, left_edge, right_edge, back_edge, [bottom])) + result = vnf_join(concat(tiled_vnf,front_back_closed, sides_closed,base,[bottom])) ) move([-size.x/2,-size.y/2],result), trans_vnf = is_undef(h_w1_w2_shift) ? vnf diff --git a/skin.scad b/skin.scad index fae5a53d..edeb0058 100644 --- a/skin.scad +++ b/skin.scad @@ -14,6 +14,7 @@ // FileFootnotes: STD=Included in std.scad ////////////////////////////////////////////////////////////////////// +__vnf_no_n_mesg=" texture is a VNF so it does not accept n. Set sample rate for VNF textures using the tex_samples parameter to cyl(), linear_sweep() or rotate_sweep()."; // Section: Skin and sweep @@ -1570,6 +1571,11 @@ module spiral_sweep(poly, h, r, turns=1, taper, r1, r2, d, d1, d2, internal=fals // the object's VNF data. The center of the object is determined based on the `cp` argument and can be "centroid" (the default), "mean" to use the mean of the object, // or "box" to use the center of the bounding box. For complicated objects you may find it difficult to get useful results from the anchoring // system, which is designed for an object whose center is inside the object. When using an anchors, confirm that it is in the location you desire. +// . +// You can apply a texture to the path sweep object using the usual texture parameters. +// See [Texturing](skin.scad#section-texturing) for more details on how textures work. +// This works by passing through to {{vnf_vertex_array()}}, which also has more details on +// texturing. Note that textures only work when the shape is a path; you cannot apply a texture to a region. // Arguments: // shape = A 2D polygon path or region describing the shape to be swept. // path = 2D or 3D path giving the path to sweep over @@ -1592,6 +1598,15 @@ module spiral_sweep(poly, h, r, turns=1, taper, r1, r2, d, d1, d2, internal=fals // width = the width of lines used for profile display. (module only) Default: 1 // transforms = set to true to return transforms instead of a VNF. These transforms can be manipulated and passed to sweep(). (function only) Default: false. // convexity = convexity parameter for polyhedron(). (module only) Default: 10 +// 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 // anchor = Translate so anchor point is at the origin. Default: "origin" // spin = Rotate this many degrees around Z axis after anchor. Default: 0 // orient = Vector to rotate top towards after spin @@ -1908,9 +1923,14 @@ module spiral_sweep(poly, h, r, turns=1, taper, r1, r2, d, d1, d2, internal=fals // attach("end") stroke([path3d(yscale(1.5,shape))],width=.5); // } + + + module path_sweep(shape, path, method="incremental", normal, closed, twist=0, twist_by_length=true, scale=1, scale_by_length=true, - symmetry=1, last_normal, tangent, uniform=true, relaxed=false, caps, style="min_edge", convexity=10, - anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull",profiles=false,width=1) + symmetry=1, last_normal, tangent, uniform=true, relaxed=false, caps, style="min_edge", convexity=10, + anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull",profiles=false,width=1, + texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, + tex_depth=1, tex_extra, tex_skip) { dummy = assert(is_region(shape) || is_path(shape,2), "shape must be a 2D path or region") assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\""); @@ -1923,7 +1943,9 @@ module path_sweep(shape, path, method="incremental", normal, closed, twist=0, tw scales = trans_scale[1]; firstscale = is_num(scales[0]) ? 1/scales[0] : [1/scales[0].x, 1/scales[0].y]; lastscale = is_num(last(scales)) ? 1/last(scales) : [1/last(scales).x, 1/last(scales).y]; - vnf = sweep(is_path(shape)?clockwise_polygon(shape):shape, transforms, closed=false, caps=fullcaps,style=style); + vnf = sweep(is_path(shape)?clockwise_polygon(shape):shape, transforms, closed=false, _closed_for_normals=closed, caps=fullcaps,style=style, + 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); shapecent = point3d(centroid(shape)); $sweep_transforms = transforms; $sweep_scales = scales; @@ -1949,7 +1971,7 @@ module path_sweep(shape, path, method="incremental", normal, closed, twist=0, tw } else attachable(anchor,spin,orient,vnf=vnf,extent=atype=="hull", cp=cp,anchors=anchors){ - vnf_polyhedron(vnf,convexity=convexity); + vnf_polyhedron(vnf,convexity=convexity); children(); } } @@ -1957,10 +1979,14 @@ module path_sweep(shape, path, method="incremental", normal, closed, twist=0, tw function path_sweep(shape, path, method="incremental", normal, closed, twist=0, twist_by_length=true, scale=1, scale_by_length=true, symmetry=1, last_normal, tangent, uniform=true, relaxed=false, caps, style="min_edge", transforms=false, + texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, + tex_depth=1, tex_extra, tex_skip, anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull",_return_scales=false) = is_1region(path) ? path_sweep(shape=shape,path=path[0], method=method, normal=normal, closed=default(closed,true), twist=twist, scale=scale, scale_by_length=scale_by_length, twist_by_length=twist_by_length, symmetry=symmetry, last_normal=last_normal, tangent=tangent, uniform=uniform, relaxed=relaxed, caps=caps, style=style, transforms=transforms, + texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, + tex_depth=1, tex_extra, tex_skip, anchor=anchor, cp=cp, spin=spin, orient=orient, atype=atype, _return_scales=_return_scales) : let(closed=default(closed,false)) assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"") @@ -2117,7 +2143,11 @@ function path_sweep(shape, path, method="incremental", normal, closed, twist=0, ? [transform_list,scale] : transforms ? transform_list : sweep(is_path(shape)?clockwise_polygon(shape):shape, transform_list, closed=false, caps=fullcaps,style=style, - anchor=anchor,cp=cp,spin=spin,orient=orient,atype=atype); + anchor=anchor,cp=cp,spin=spin,orient=orient,atype=atype, + 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, + _closed_for_normals=closed + ); // Function&Module: path_sweep2d() @@ -2276,6 +2306,12 @@ function _ofs_face_edge(face,firstlen,second=false) = // overlooked self-intersection. Note also that the errors will not occur when your shape is alone // in your model, but will arise if you add a second object to the model. This may mislead you into // thinking the second object caused a problem. Even adding a simple cube to the model will reveal the problem. +// . +// You can apply a texture to the sweep object using the usual texture parameters. +// See [Texturing](skin.scad#section-texturing) for more details on how textures work. +// This works by passing through to {{vnf_vertex_array()}}, which also has more details on +// texturing. Note that textures only work when the shape is a path; you cannot apply a texture to a region. +// The texture tiles are oriented on the path sweep so that the Y axis of the tile is aligned with the sweep direction. // Arguments: // shape = 2d path or region, describing the shape to be swept. // transforms = list of 4x4 matrices to apply @@ -2284,6 +2320,15 @@ function _ofs_face_edge(face,firstlen,second=false) = // style = vnf_vertex_array style. Default: "min_edge" // --- // convexity = convexity setting for use with polyhedron. (module only) Default: 10 +// 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 // 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" // atype = Select "hull" or "intersect" anchor types. Default: "hull" // anchor = Translate so anchor point is at the origin. Default: "origin" @@ -2306,7 +2351,7 @@ function _ofs_face_edge(face,firstlen,second=false) = // function rotate(t) = 180 * pow((1 - t), 3); // step = 0.01; // path_transforms = [for (t=[0:step:1-step]) translate(path(t)) * zrot(rotate(t)) * scale([drop(t), drop(t), 1])]; -// sweep(circle(1, $fn=12), path_transforms); +// sweep(reverse(circle(1, $fn=12)), path_transforms); // Example: Another example from list-comprehension-demos // function f(x) = 3 - 2.5 * x; // function r(x) = 2 * 180 * x * x * x; @@ -2320,9 +2365,19 @@ function _ofs_face_edge(face,firstlen,second=false) = // outside = [for(i=[0:24]) up(i)*rot(i)*scale(1.25*i/24+1)]; // inside = [for(i=[24:-1:2]) up(i)*rot(i)*scale(1.2*i/24+1)]; // sweep(shape, concat(outside,inside)); +// Example: "sweet-drop" with a dots texture +// function drop(t) = 100 * 0.5 * (1 - cos(180 * t)) * sin(180 * t) + 1; +// function path(t) = [0, 0, 80 + 80 * cos(180 * t)]; +// function rotate(t) = 180 * pow((1 - t), 3); +// step = 0.01; +// path_transforms = [for (t=[0:step:1-step]) translate(path(t)) * zrot(rotate(t)) * scale([drop(t), drop(t), 1])]; +// sweep(reverse(circle(1, $fn=12)), path_transforms, texture="dots", tex_reps=[12,12],tex_depth=.1); + function sweep(shape, transforms, closed=false, caps, style="min_edge", - anchor="origin", cp="centroid", spin=0, orient=UP, atype="hull") = + anchor="origin", cp="centroid", spin=0, orient=UP, atype="hull", + texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, + tex_depth=1, tex_extra, tex_skip, _closed_for_normals=false) = assert(is_consistent(transforms, ident(4)), "Input transforms must be a list of numeric 4x4 matrices in sweep") assert(is_path(shape,2) || is_region(shape), "Input shape must be a 2d path or a region.") let( @@ -2334,31 +2389,49 @@ function sweep(shape, transforms, closed=false, caps, style="min_edge", assert(len(transforms)>=2, "transformation must be length 2 or more") assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") - is_region(shape)? let( - regions = region_parts(shape), - rtrans = reverse(transforms), - vnfs = [ - for (rgn=regions) each [ - for (path=rgn) - sweep(path, transforms, closed=closed, caps=false, style=style), - if (fullcaps[0]) vnf_from_region(rgn, transform=transforms[0], reverse=true), - if (fullcaps[1]) vnf_from_region(rgn, transform=last(transforms)), + is_region(shape)? + assert(is_undef(texture), "textures are not supported for regions, only paths") + let( + regions = region_parts(shape), + rtrans = reverse(transforms), + vnfs = [ + for (rgn=regions) each [ + for (path=rgn) + sweep(path, transforms, closed=closed, caps=false, style=style), + if (fullcaps[0]) vnf_from_region(rgn, transform=transforms[0], reverse=true), + if (fullcaps[1]) vnf_from_region(rgn, transform=last(transforms)), + ], ], - ], - vnf = vnf_join(vnfs) - ) vnf : + vnf = vnf_join(vnfs) + ) + vnf + : assert(len(shape)>=3, "shape must be a path of at least 3 non-colinear points") - vnf_vertex_array([for(i=[0:len(transforms)-(closed?0:1)]) apply(transforms[i%len(transforms)],path3d(shape))], - cap1=fullcaps[0],cap2=fullcaps[1],col_wrap=true,style=style); + let( + points = [for(i=[0:len(transforms)-(closed?0:1)]) apply(transforms[i%len(transforms)],path3d(shape))], + normals = (!(is_def(texture) && (closed || _closed_for_normals))) ? undef + : let( + n = surfnormals(select(points,0,-2), col_wrap=true, row_wrap=true) + ) + [each n, n[0]] + ) + vnf_vertex_array(points, normals=normals, + cap1=fullcaps[0],cap2=fullcaps[1],col_wrap=true,style=style, + 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); module sweep(shape, transforms, closed=false, caps, style="min_edge", convexity=10, - anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull") + anchor="origin",cp="centroid",spin=0, orient=UP, atype="hull", + texture, tex_reps, tex_size, tex_samples, tex_inset=false, tex_rot=0, + tex_depth=1, tex_extra, tex_skip) { $sweep_transforms=transforms; $sweep_shape=shape; $sweep_closed=closed; - vnf = sweep(shape, transforms, closed, caps, style); + vnf = sweep(shape, transforms, closed, caps, style, + 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); vnf_polyhedron(vnf, convexity=convexity, anchor=anchor, spin=spin, orient=orient, atype=atype, cp=cp) children(); } @@ -3563,7 +3636,7 @@ function associate_vertices(polygons, split, curpoly=0) = function _tex_fn_default() = 16; -__vnf_no_n_mesg=" texture is a VNF so it does not accept n. Set sample rate for VNF textures using the tex_samples parameter to cyl(), linear_sweep() or rotate_sweep()."; + function texture(tex, n, border, gap, roughness, inset) = assert(num_defined([border,inset])<2, "In texture() the 'inset' parameter has been replaced by 'border'. You cannot give both parameters.") @@ -4630,7 +4703,7 @@ 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) = + col_wrap=false, tex_depth=1, row_wrap=false, caps, cap1, cap2, reverse=false, style="min_edge", tex_extra, tex_skip, sidecaps,sidecap1,sidecap2,normals) = 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") @@ -4654,7 +4727,7 @@ function _texture_point_array(points, texture, tex_reps, tex_size, tex_samples, 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), + normals = default(normals,surfnormals(points, col_wrap=col_wrap, row_wrap=row_wrap)), getscale = function(x,y) (x+y)/2 ) !is_vnf(texture) ? // heightmap case @@ -4690,8 +4763,8 @@ function _texture_point_array(points, texture, tex_reps, tex_size, tex_samples, 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_vertex_array(tex_surf, row_wrap=row_wrap, col_wrap=col_wrap, reverse=reverse,style=style, caps=caps, cap1=cap1, cap2=cap2, triangulate=triangulate) : // VNF case let( local_scale = [for(y=[-1:1:ptsize.y-1]) @@ -4712,6 +4785,7 @@ function _texture_point_array(points, texture, tex_reps, tex_size, tex_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]) [ @@ -4730,6 +4804,8 @@ function _texture_point_array(points, texture, tex_reps, tex_size, tex_samples, 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), diff --git a/tests/test_regions.scad b/tests/test_regions.scad index 5f9499cd..fbd22451 100644 --- a/tests/test_regions.scad +++ b/tests/test_regions.scad @@ -134,7 +134,7 @@ module test_make_region(){ 22.451398829]], [[69.0983005625, 22.451398829], [50, 36.3271264003], [80.9016994375, 58.7785252292]], [[61.803398875, 3.5527136788e-15], [69.0983005625, 22.451398829], [100, 0]], [[38.196601125, 0], - [61.803398875, 3.94430452611e-31], [50, -36.3271264003]]])); + [61.803398875, 3.94430452611e-31], [50, -36.3271264003]]], either_winding=true)); /*assert_approx(region1, [[[0, 0], [38.196601125, 0], [30.9016994375, 22.451398829]], [[50, 36.3271264003], [19.0983005625, 58.7785252292], [30.9016994375, diff --git a/vnf.scad b/vnf.scad index c791c130..48c4bb91 100644 --- a/vnf.scad +++ b/vnf.scad @@ -64,7 +64,11 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. // 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. +// count equal to the tile count times tex_samples, plus one if wrapping is disabled. +// . +// For creating the texture, `vnf_vertex_array()` uses normals to the surface that it estimates from the surface data itself. +// If you have more accurate normals or need the normals to take particular values, you can pass an array of normals +// using the `normals` parameter. // Arguments: // points = A list of vertices to divide into columns and rows. // --- @@ -89,6 +93,7 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. // 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 +// normals = array of normal vectors to each point in the point array for more accurate texture height calculation // 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` @@ -218,8 +223,8 @@ module vnf_vertex_array( 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(); + ); + vnf_polyhedron(vnf, convexity=convexity, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull") children(); } @@ -232,7 +237,7 @@ function vnf_vertex_array( 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 + tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2, normals ) = 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") @@ -242,7 +247,7 @@ function vnf_vertex_array( _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) + style=style, tex_extra=tex_extra, tex_skip=tex_skip, sidecaps=sidecaps, sidecap1=sidecap1, sidecap2=sidecap2,normals=normals,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)")