From 20eae2b5d24df62b2d65498dfadd11989c69e5ce Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Thu, 18 Jul 2019 19:21:08 -0400 Subject: [PATCH] Changed offset() to check polygon orientation when closed==true. Changed rounded_sweep() to work with updated offset(). Added make_path_valid() and use it in rounded_sweep for better error handling and to support single component regions. Added divided box example to rounded_sweep. Updated examples to work with updated offset(). --- geometry.scad | 70 +++++++++++++++++++++++++++++++++------------------ rounding.scad | 67 +++++++++++++++++++++++++++--------------------- 2 files changed, 85 insertions(+), 52 deletions(-) diff --git a/geometry.scad b/geometry.scad index a8fc259..7214de5 100644 --- a/geometry.scad +++ b/geometry.scad @@ -702,6 +702,27 @@ function is_region(x) = is_list(x) && is_path(x.x); // Closes all paths within a given region. function close_region(region, eps=EPSILON) = [for (path=region) close_path(path, eps=eps)]; +// Function: make_path_valid() +// Usage: +// make_path_valid(path, [valid_dim], [closed]) +// Description: +// Checks that the input is a path. If it is a region with one component, converts it to a path. +// valid_dim specfies the allowed dimension of the points in the path. +// If the path is closed, removed duplicate endpoint if present. +function make_path_valid(path,valid_dim=undef,closed=false) = + let( path = is_region(path) ? + assert(len(path)==1,"Region supplied as path does not have exactly one component") + path[0] : + assert(is_path(path), "Input is not a path") path, + dim = array_dim(path)) + assert(dim[0]>1,"Path must have at least 2 points") + assert(len(dim)==2,"Invalid path: path is either a list of scalars or a list of matrices") + assert(is_def(dim[1]), "Invalid path: entries in the path have variable length") + let(valid=is_undef(valid_dim) || in_list(dim[1],valid_dim)) + assert(valid, str("The points on the path have length ",dim[1]," but length must be ", + len(valid_dim)==1? valid_dim[0] : str("one of ",valid_dim))) + closed && approx(path[0],select(path,-1)) ? slice(path,0,-2) : path; + // Function: region_path_crossings() // Usage: @@ -908,6 +929,18 @@ function _offset_region( // Example(2D): // star = star(5, r=100, ir=30); // #stroke(closed=true, star); +// stroke(closed=true, offset(star, delta=10, closed=true)); +// Example(2D): +// star = star(5, r=100, ir=30); +// #stroke(closed=true, star); +// stroke(closed=true, offset(star, delta=10, chamfer=true, closed=true)); +// Example(2D): +// star = star(5, r=100, ir=30); +// #stroke(closed=true, star); +// stroke(closed=true, offset(star, r=10, closed=true)); +// Example(2D): +// star = star(5, r=100, ir=30); +// #stroke(closed=true, star); // stroke(closed=true, offset(star, delta=-10, closed=true)); // Example(2D): // star = star(5, r=100, ir=30); @@ -917,46 +950,34 @@ function _offset_region( // star = star(5, r=100, ir=30); // #stroke(closed=true, star); // stroke(closed=true, offset(star, r=-10, closed=true)); -// Example(2D): -// star = star(5, r=100, ir=30); -// #stroke(closed=true, star); -// stroke(closed=true, offset(star, delta=10, closed=true)); -// Example(2D): -// star = star(5, r=100, ir=30); -// #stroke(closed=true, star); -// stroke(closed=true, offset(star, delta=-10, chamfer=true, closed=true)); -// Example(2D): -// star = star(5, r=100, ir=30); -// #stroke(closed=true, star); -// stroke(closed=true, offset(star, r=10, closed=true)); // Example(2D): This case needs `quality=2` for success // test = [[0,0],[10,0],[10,7],[0,7], [-1,-3]]; -// polygon(offset(test,r=1.9, closed=true, quality=2)); -// //polygon(offset(test,r=1.9, closed=true, quality=1)); // Fails with erroneous 180 deg path error +// polygon(offset(test,r=-1.9, closed=true, quality=2)); +// //polygon(offset(test,r=-1.9, closed=true, quality=1)); // Fails with erroneous 180 deg path error // %down(.1)polygon(test); // Example(2D): This case fails if `check_valid=true` when delta is large enough because segments are too close to the opposite side of the curve. // star = star(5, r=22, ir=13); -// stroke(star,width=.1,closed=true); +// stroke(star,width=.2,closed=true); // color("green") -// stroke(offset(star, delta=9, closed=true),width=.1,closed=true); // Works with check_valid=true (the default) +// stroke(offset(star, delta=-9, closed=true),width=.2,closed=true); // Works with check_valid=true (the default) // color("red") -// stroke(offset(star, delta=10, closed=true, check_valid=false), // Fails if check_valid=true -// width=.1,closed=true); +// stroke(offset(star, delta=-10, closed=true, check_valid=false), // Fails if check_valid=true +// width=.2,closed=true); // Example(2D): But if you use rounding with offset then you need `check_valid=true` when `r` is big enough. It works without the validity check as long as the offset shape retains a some of the straight edges at the star tip, but once the shape shrinks smaller than that, it fails. There is no simple way to get a correct result for the case with `r=10`, because as in the previous example, it will fail if you turn on validity checks. // star = star(5, r=22, ir=13); // color("green") -// stroke(offset(star, r=8, closed=true,check_valid=false), width=.1, closed=true); +// stroke(offset(star, r=-8, closed=true,check_valid=false), width=.1, closed=true); // color("red") -// stroke(offset(star, r=10, closed=true,check_valid=false), width=.1, closed=true); +// stroke(offset(star, r=-10, closed=true,check_valid=false), width=.1, closed=true); // Example(2D): The extra triangles in this example show that the validity check cannot be skipped -// ellipse = scale([20,4], p=circle(r=1)); +// ellipse = scale([20,4], p=circle(r=1,$fn=64)); // stroke(ellipse, closed=true, width=0.3); // stroke(offset(ellipse, r=-3, check_valid=false, closed=true), width=0.3, closed=true); // Example(2D): The triangles are removed by the validity check -// ellipse = scale([20,4], p=circle(r=1)); +// ellipse = scale([20,4], p=circle(r=1,$fn=64)); // stroke(ellipse, closed=true, width=0.3); // stroke(offset(ellipse, r=-3, check_valid=true, closed=true), width=0.3, closed=true); -// Example(2D): +// Example(2D): Open path. The path moves from left to right and the positive offset shifts to the left of the initial red path. // sinpath = 2*[for(theta=[-180:5:180]) [theta/4,45*sin(theta)]]; // #stroke(sinpath); // stroke(offset(sinpath, r=17.5)); @@ -994,7 +1015,8 @@ function offset( let( chamfer = is_def(r) ? false : chamfer, quality = max(0,round(quality)), - d = is_def(r)? r : delta, + flip_dir = closed && !polygon_clockwise(path) ? -1 : 1, + d = flip_dir * (is_def(r) ? r : delta), shiftsegs = [for(i=[0:len(path)-1]) _shift_segment(select(path,i,i+1), d)], // good segments are ones where no point on the segment is less than distance d from any point on the path good = check_valid ? _good_segments(path, abs(d), shiftsegs, closed, quality) : replist(true,len(shiftsegs)), diff --git a/rounding.scad b/rounding.scad index d966fbc..6a344ec 100644 --- a/rounding.scad +++ b/rounding.scad @@ -374,10 +374,10 @@ function bezier_curve(P,N) = // - "steps" - number of vertical steps to use for the roundover. Default: 16. // - "offset_maxstep" - maxstep distance for offset() calls; controls the horizontal step density. Default: 1 // - "offset" - select "round" (r=) or "delta" (delta=) offset type for offset. Default: "round" -// +// // You can change the the defaults by passing an argument to the rounded_sweep, which is more convenient if you want // a setting to be the same at both ends. -// +// // You can use several helper functions to provide the rounding spec. These use function arguments to set the same parameters listed above, where the // function name indicates the type of rounding and only parameters valid for that rounding type are accepted: // - rs_circle(r,cut,extra,check_valid, quality,steps, offset_maxstep, offset) @@ -385,11 +385,11 @@ function bezier_curve(P,N) = // - rs_chamfer(height, width, cut, extra,check_valid, quality,steps, offset_maxstep, offset) // - rs_smooth(cut, joint, extra,check_valid, quality,steps, offset_maxstep, offset) // - rs_custom(points, extra,check_valid, quality,steps, offset_maxstep, offset) -// +// // For example, you could round a path using `rounded_sweep(path, top=rs_teardrop(r=10), bottom=rs_chamfer(height=-10,extra=1))` // Many of the arguments are described as setting "default" values because they establish settings which may be overridden by // the top and bottom rounding specifications. -// +// // Arguments: // path = 2d path (list of points) to extrude // height = total height (including rounded portions, but not extra sections) of the output @@ -408,7 +408,7 @@ function bezier_curve(P,N) = // joint = default joint value for smooth roundover. // k = default curvature parameter value for "smooth" roundover // convexity = convexity setting for use with polyhedron. Default: 10 -// +// // Example: Rounding a star shaped prism with postive radius values // star = star(5, r=22, ir=13); // rounded_star = round_corners(zip(star, flatten(replist([.5,0],5))), curve="circle", measure="cut", $fn=12); @@ -441,7 +441,7 @@ function bezier_curve(P,N) = // roundbox = round_corners(smallbox, curve="smooth", measure="cut", size=4, $fn=36); // thickness=4; // height=50; -// back_half(y=37, s=200) +// back_half(y=25, s=200) // difference(){ // rounded_sweep(roundbox, height=height, bottom=["r",10,"type","teardrop"], top=["r",2], steps = 22, check_valid=false); // up(thickness) @@ -450,6 +450,24 @@ function bezier_curve(P,N) = // bottom=["r",6], // top=["type","chamfer","angle",30,"height",-3,"extra",1,"check_valid",false]); // } +// Example: A box with multiple sections and rounded dividers +// thickness = 2; +// box = ([[0,0], [0,50], [255,50], [255,0]]); +// cutpoints = [0, 125, 190, 255]; +// rbox = round_corners(box, curve="smooth", measure="cut", size=4, $fn=36); +// back_half(y=25, s=700) +// difference(){ +// rounded_sweep(rbox, height=50, check_valid=false, steps=22, bottom=rs_teardrop(r=2), top=rs_circle(r=1)); +// up(thickness) +// for(i=[0:2]){ +// ofs = i==1 ? 2 : 0; +// hole = round_corners([[cutpoints[i]-ofs,0], [cutpoints[i]-ofs,50], [cutpoints[i+1]+ofs, 50], [cutpoints[i+1]+ofs,0]], +// curve="smooth", measure="cut", size=4, $fn=36); +// rounded_sweep(offset(hole, r=-thickness, closed=true,check_valid=false), +// height=48, steps=22, check_valid=false, bottom=rs_circle(r=4), top=rs_circle(r=-1,extra=1)); +// +// } +// } // Example: Star shaped box // star = star(5, r=22, ir=13); // rounded_star = round_corners(zip(star, flatten(replist([.5,0],5))), curve="circle", measure="cut", $fn=12); @@ -458,7 +476,7 @@ function bezier_curve(P,N) = // difference(){ // rounded_sweep(rounded_star, height=ht, bottom=["r",4], top=["r",1], steps=15); // up(thickness) -// rounded_sweep(offset(rounded_star,r=thickness,closed=true), +// rounded_sweep(offset(rounded_star,r=-thickness,closed=true), // height=ht-thickness, check_valid=false, // bottom=rs_circle(r=7), top=rs_circle(r=-1, extra=1)); // } @@ -488,7 +506,7 @@ function bezier_curve(P,N) = // up(1) // rounded_sweep(offset(rhex,r=1), height=9.5, bottom=rs_circle(r=2), top=rs_teardrop(r=-4)); // } -module rounded_sweep(path, height, top=[], bottom=[], offset="round", r=undef, steps=16, quality=1, check_valid=true, offset_maxstep=1, extra=0, +module rounded_sweep(path, height, top=[], bottom=[], offset="round", r=0, steps=16, quality=1, check_valid=true, offset_maxstep=1, extra=0, cut=undef, width=undef, joint=undef, k=0.75, angle=45, convexity=10) { // This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce @@ -514,13 +532,13 @@ module rounded_sweep(path, height, top=[], bottom=[], offset="round", r=undef, s // Produce edge profile curve from the edge specification // z_dir is the direction multiplier (1 to build up, -1 to build down) - function rounding_offsets(edgespec,flipR,z_dir=1) = + function rounding_offsets(edgespec,z_dir=1) = let( edgetype = struct_val(edgespec, "type"), extra = struct_val(edgespec,"extra"), N = struct_val(edgespec, "steps"), - r = flipR * struct_val(edgespec,"r"), - cut = flipR * struct_val(edgespec,"cut"), + r = struct_val(edgespec,"r"), + cut = struct_val(edgespec,"cut"), k = struct_val(edgespec,"k"), radius = in_list(edgetype,["circle","teardrop"]) ? first_defined([cut/(sqrt(2)-1),r]) : @@ -528,10 +546,10 @@ module rounded_sweep(path, height, top=[], bottom=[], offset="round", r=undef, s undef, chamf_angle = struct_val(edgespec, "angle"), cheight = struct_val(edgespec, "height"), - cwidth = flipR * struct_val(edgespec, "width"), + cwidth = struct_val(edgespec, "width"), chamf_width = first_defined([cut/cos(chamf_angle), cwidth, cheight*tan(chamf_angle)]), chamf_height = first_defined([cut/sin(chamf_angle),cheight, cwidth/tan(chamf_angle)]), - joint = first_defined([flipR*struct_val(edgespec,"joint"), + joint = first_defined([struct_val(edgespec,"joint"), 16*cut/sqrt(2)/(1+4*k)]), points = struct_val(edgespec, "points"), argsOK = in_list(edgetype,["circle","teardrop"]) ? is_def(radius) : @@ -541,7 +559,7 @@ module rounded_sweep(path, height, top=[], bottom=[], offset="round", r=undef, s false) assert(argsOK,str("Invalid specification with type ",edgetype)) let( - offsets = edgetype == "custom" ? scale([-flipR,z_dir], slice(points,1,-1)) : + offsets = edgetype == "custom" ? scale([-1,z_dir], slice(points,1,-1)) : edgetype == "chamfer" ? width==0 && height==0 ? [] : [[-chamf_width,z_dir*abs(chamf_height)]] : edgetype == "teardrop" ? radius==0 ? [] : concat([for(i=[1:N]) [radius*(cos(i*45/N)-1),z_dir*abs(radius)* sin(i*45/N)]], [[-2*radius*(1-sqrt(2)/2), z_dir*abs(radius)]]): @@ -554,8 +572,8 @@ module rounded_sweep(path, height, top=[], bottom=[], offset="round", r=undef, s extra > 0 ? concat(offsets, [select(offsets,-1)+[0,z_dir*extra]]) : offsets; - argspec = [["r",0], - ["extra",0], + argspec = [["r",r], + ["extra",extra], ["type","circle"], ["check_valid",check_valid], ["quality",quality], @@ -570,13 +588,13 @@ module rounded_sweep(path, height, top=[], bottom=[], offset="round", r=undef, s ["k", k], ["points", []], ]; + + path = make_path_valid(path, [2], closed=true); + top = struct_set(argspec, top, grow=false); bottom = struct_set(argspec, bottom, grow=false); - struct_echo(top,"top"); - clockwise = polygon_clockwise(path); - flipR = clockwise ? 1 : -1; assert(height>=0, "Height must be nonnegative"); @@ -587,11 +605,9 @@ module rounded_sweep(path, height, top=[], bottom=[], offset="round", r=undef, s assert(offsetsok,"Offsets must be one of \"round\" or \"delta\""); */ - offsets_bot = rounding_offsets(bottom, flipR,-1); - offsets_top = rounding_offsets(top, flipR,1); + offsets_bot = rounding_offsets(bottom, -1); + offsets_top = rounding_offsets(top, 1); - echo(ofstop = offsets_top); - // "Extra" height enlarges the result beyond the requested height, so subtract it bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra"); top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra"); @@ -616,11 +632,6 @@ module rounded_sweep(path, height, top=[], bottom=[], offset="round", r=undef, s up(bottom_height) polyhedron(concat(vertices_faces_bot[0],vertices_faces_top[0]), faces=concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces),convexity=convexity); - echo(botv=vertices_faces_bot[0]); - echo(topv=vertices_faces_top[0]); - echo(fbot=vertices_faces_bot[1]); - echo(ftop=vertices_faces_top[1]); - echo(fmid=middle_faces); } function rs_circle(r,cut,extra,check_valid, quality,steps, offset_maxstep, offset) =