diff --git a/regions.scad b/regions.scad index 15aa560..d903177 100644 --- a/regions.scad +++ b/regions.scad @@ -58,35 +58,34 @@ module region(r) // Function: check_and_fix_path() // Usage: -// check_and_fix_path(path, [valid_dim], [closed]) +// check_and_fix_path(path, [valid_dim], [closed], [name]) // Description: // Checks that the input is a path. If it is a region with one component, converts it to a path. +// Note that arbitrary paths must have at least two points, but closed paths need at least 3 points. // valid_dim specfies the allowed dimension of the points in the path. -// If the path is closed, removed duplicate endpoint if present. +// If the path is closed, removes duplicate endpoint if present. // Arguments: // path = path to process // valid_dim = list of allowed dimensions for the points in the path, e.g. [2,3] to require 2 or 3 dimensional input. If left undefined do not perform this check. Default: undef // closed = set to true if the path is closed, which enables a check for endpoint duplication -function check_and_fix_path(path, valid_dim=undef, closed=false) = +// name = parameter name to use for reporting errors. Default: "path" +function check_and_fix_path(path, valid_dim=undef, closed=false, name="path") = 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) + path = + is_region(path)? + assert(len(path)==1,str("Region ",name," supplied as path does not have exactly one component")) + path[0] + : + assert(is_path(path), str("Input ",name," is not a path")) + 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(len(path)>(closed?2:1),closed?str("Closed path ",name," must have at least 3 points") + :str("Path ",name," must have at least 2 points")) + let(valid=is_undef(valid_dim) || in_list(len(path[0]),force_list(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) + "Input ",name," must has dimension ", len(path[0])," but dimension must be ", + is_list(valid_dim) ? str("one of ",valid_dim) : valid_dim ) ) closed && approx(path[0],select(path,-1))? slice(path,0,-2) : path; @@ -223,6 +222,64 @@ function split_nested_region(region) = ) outs; +function find_first_approx(val, list, start=0, all=false, eps=EPSILON) = + all? [for (i=[start:1:len(list)-1]) if(approx(val, list[i], eps=eps)) i] : + __find_first_approx(val, list, eps=eps, i=start); + +function __find_first_approx(val, list, eps, i=0) = + i >= len(list)? undef : + approx(val, list[i], eps=eps)? i : + __find_first_approx(val, list, eps=eps, i=i+1); + + +// Function: polygons_equal() +// Usage: +// b = polygons_equal(poly1, poly2, ) +// Description: +// Returns true if the components of region1 and region2 are the same polygons +// within given epsilon tolerance. +// Arguments: +// poly1 = first polygon +// poly2 = second polygon +// eps = tolerance for comparison +// Example(NORENDER): +// polygons_equal(pentagon(r=4), +// rot(360/5, p=pentagon(r=4))); // returns true +// polygons_equal(pentagon(r=4), +// rot(90, p=pentagon(r=4))); // returns false +function polygons_equal(poly1, poly2, eps=EPSILON) = + let( l1 = len(poly1), l2 = len(poly2)) + l1 != l2 ? false : + let( maybes = find_first_approx(poly1[0], poly2, eps=eps, all=true) ) + maybes == []? false : + [for (i=maybes) if (__polygons_equal(poly1, poly2, eps, i)) 1] != []; + +function __polygons_equal(poly1, poly2, eps, st) = + max([for(d=poly1-select(poly2,st,st-1)) d*d])) +// Description: +// Returns true if the components of region1 and region2 are the same polygons +// within given epsilon tolerance. +// Arguments: +// poly1 = first polygon +// poly2 = second polygon +// eps = tolerance for comparison +function regions_equal(region1, region2) = + assert(is_region(region1) && is_region(region2)) + len(region1)==len(region2) && + [ + for (a=region1) + if (1!=sum( + [for(b=region2) + if (polygons_equal(path3d(a), path3d(b))) + 1] + ) + ) 1 + ] == []; + // Section: Region Extrusion and VNFs diff --git a/rounding.scad b/rounding.scad index b9ef299..afebedb 100644 --- a/rounding.scad +++ b/rounding.scad @@ -75,7 +75,7 @@ include // circular roundovers. For continuous curvature roundovers `$fs` and `$fn` are used and `$fa` is // ignored. Note that $fn is interpreted as the number of points on the roundover curve, which is // not equivalent to its meaning for rounding circles because roundovers are usually small fractions -// of a circular arc. When doing continuous curvature rounding be sure to use lots of segments or the effect +// of a circular arc. As usual, $fn overrides $fs. When doing continuous curvature rounding be sure to use lots of segments or the effect // will be hidden by the discretization. Note that if you use $fn with "smooth" then $fn points are added at each corner, even // if the "corner" is flat, with collinear points, so this guarantees a specific output length. // @@ -264,8 +264,7 @@ function round_corners(path, method="circle", radius, cut, joint, k, closed=true let( pathbit = select(path,i-1,i+1), angle = approx(pathbit[0],pathbit[1]) || approx(pathbit[1],pathbit[2]) ? undef - : vector_angle(select(path,i-1,i+1))/2, - f=echo(angle=angle) + : vector_angle(select(path,i-1,i+1))/2 ) (!closed && (i==0 || i==len(path)-1)) ? [0] : // Force zeros at ends for non-closed parm[i]==0 ? [0] : // If no rounding requested then don't try to compute parameters diff --git a/shapes2d.scad b/shapes2d.scad index 4c0d03e..a9b2d24 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -554,7 +554,7 @@ function arc(N, r, angle, d, cp, points, width, thickness, start, wedge=false, l ) assert(is_vector(cp,2),"Centerpoint must be a 2d vector") assert(angle!=0, "Arc has zero length") - assert(r>0, "Arc radius invalid") + assert(is_def(r) && r>0, "Arc radius invalid") let( N = max(3, is_undef(N)? ceil(segs(r)*abs(angle)/360) : N), arcpoints = [for(i=[0:N-1]) let(theta = start + i*angle/(N-1)) r*[cos(theta),sin(theta)]+cp], diff --git a/skin.scad b/skin.scad index 7fad7b7..de15cda 100644 --- a/skin.scad +++ b/skin.scad @@ -63,8 +63,8 @@ // Note that when dealing with continuous curves it is always better to adjust the // sampling in your code to generate the desired sampling rather than using the `refine` argument. // . -// You can choose from four methods for specifying alignment for incommensurate profiles. -// The available methods are `"distance"`, `"tangent"`, `"direct"` and `"reindex"`. +// You can choose from five methods for specifying alignment for incommensurate profiles. +// The available methods are `"distance"`, `"fast_distance"`, `"tangent"`, `"direct"` and `"reindex"`. // It is useful to distinguish between continuous curves like a circle and discrete profiles // like a hexagon or star, because the algorithms' suitability depend on this distinction. // . @@ -87,14 +87,17 @@ // `sampling="segment"` may produce a more pleasing result. These two approaches differ only when // the segments of your input profiles have unequal length. // . -// The "distance" and "tangent" methods work by duplicating vertices to create +// The "distance", "fast_distance" and "tangent" methods work by duplicating vertices to create // triangular faces. The "distance" method finds the global minimum distance method for connecting two // profiles. This algorithm generally produces a good result when both profiles are discrete ones with // a small number of vertices. It is computationally intensive (O(N^3)) and may be // slow on large inputs. The resulting surfaces generally have curved faces, so be // sure to select a sufficiently large value for `slices` and `refine`. Note that for // this method, `sampling` must be set to `"segment"`, and hence this is the default setting. -// Using sampling by length would ignore the repeated vertices and ruin the alignment. +// Using sampling by length would ignore the repeated vertices and ruin the alignment. +// The "fast_distance" method is similar to "distance", but it makes the assumption that an edge should +// connect the first vertices of the two polygons. This reduces the run time to O(N^2) and makes +// the method usable on profiles with more points if you take care to index the inputs to match. // . // The `"tangent"` method generally produces good results when // connecting a discrete polygon to a convex, finely sampled curve. It works by finding @@ -104,7 +107,9 @@ // polygon using triangular faces. Using `refine` with this method will have little effect on the model, so // you should do it only for agreement with other profiles, and these models are linear, so extra slices also // have no effect. For best efficiency set `refine=1` and `slices=0`. As with the "distance" method, refinement -// must be done using the "segment" sampling scheme to preserve alignment across duplicated points. +// must be done using the "segment" sampling scheme to preserve alignment across duplicated points. +// Note that the "tangent" method produces similar results to the "distance" method on curved inputs. If this +// method fails due to concavity, "fast_distance" may be a good option. // . // It is possible to specify `method` and `refine` as arrays, but it is important to observe // matching rules when you do this. If a pair of profiles is connected using "tangent" or "distance" @@ -119,11 +124,11 @@ // profiles = list of 2d or 3d profiles to be skinned. (If 2d must also give `z`.) // 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. // --- -// 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" method. Default: 1. -// sampling = sampling method to use with "direct" and "reindex" methods. Can be "length" or "segment". Ignored if any profile pair uses either the "distance" or "tangent" methods. Default: "length". +// 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. +// 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". // closed = set to true to connect first and last profile (to make a torus). Default: false // caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false. -// method = method for connecting profiles, one of "distance", "tangent", "direct" or "reindex". Default: "direct". +// method = method for connecting profiles, one of "distance", "fast_distance", "tangent", "direct" or "reindex". Default: "direct". // z = array of height values for each profile if the profiles are 2d // convexity = convexity setting for use with polyhedron. (module only) Default: 10 // anchor = Translate so anchor point is at the origin. (module only) Default: "origin" @@ -374,7 +379,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close assert(len(bad)==0, str("Profiles ",bad," are not a paths or have length less than 3")) let( profcount = len(profiles) - (closed?0:1), - legal_methods = ["direct","reindex","distance","tangent"], + legal_methods = ["direct","reindex","distance","fast_distance","tangent"], caps = is_def(caps) ? caps : closed ? false : true, capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), @@ -402,7 +407,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close assert(methodlistok==[], str("method list contains invalid method at ",methodlistok)) assert(len(method) == profcount,"Method list is the wrong length") assert(in_list(sampling,["length","segment"]), "sampling must be set to \"length\" or \"segment\"") - assert(sampling=="segment" || (!in_list("distance",method) && !in_list("tangent",method)), "sampling is set to \"length\" which is only allowed iwith methods \"direct\" and \"reindex\"") + assert(sampling=="segment" || (!in_list("distance",method) && !in_list("fast_distance") && !in_list("tangent",method)), "sampling is set to \"length\" which is only allowed with methods \"direct\" and \"reindex\"") assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") let( @@ -449,6 +454,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close let( pair = method[i]=="distance" ? _skin_distance_match(profiles[i],select(profiles,i+1)) : + method[i]=="fast_distance" ? _skin_aligned_distance_match(profiles[i], select(profiles,i+1)) : method[i]=="tangent" ? _skin_tangent_match(profiles[i],select(profiles,i+1)) : /*method[i]=="reindex" || method[i]=="direct" ?*/ let( p1 = subdivide_path(profiles[i],max_list[i], method=sampling), @@ -720,6 +726,23 @@ function _skin_distance_match(poly1,poly2) = ) swap ? [newbig, newsmall] : [newsmall,newbig]; + +// This function associates vertices but with the assumption that index 0 is associated between the +// two inputs. This gives only quadratic run time. As above, output is pair of polygons with +// vertices duplicated as suited to use as input to skin(). + +function _skin_aligned_distance_match(poly1, poly2) = + let( + result = _dp_distance_array(poly1, poly2, abort_thresh=1/0), + map = _dp_extract_map(result[1]), + shift0 = len(map[0]) - max(max_index(map[0],all=true))-1, + shift1 = len(map[1]) - max(max_index(map[1],all=true))-1, + new0 = polygon_shift(repeat_entries(poly1,unique_count(map[0])[1]),shift0), + new1 = polygon_shift(repeat_entries(poly2,unique_count(map[1])[1]),shift1) + ) + [new0,new1]; + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Internal Function: _skin_tangent_match() /// Usage: @@ -927,7 +950,7 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // path_sweep(shape, path, , , , , , , , , , , , , , , , , ) ; // vnf = path_sweep(shape, path, , , , , , , , , , , , ); // Description: -// Takes as input a 2D polygon path or region, and a 2d or 3d path and constructs a polyhedron by sweeping the shape along the path. +// Takes as input a 2D polygon path, and a 2d or 3d path and constructs a polyhedron by sweeping the shape along the path. // When run as a module returns the polyhedron geometry. 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`. // . @@ -1206,6 +1229,18 @@ module sweep(shape, transforms, closed=false, caps, convexity=10, // outside = [for(i=[0:len(trans)-1]) trans[i]*scale(lerp(1,1.5,i/(len(trans)-1)))]; // inside = [for(i=[len(trans)-1:-1:0]) trans[i]*scale(lerp(1.1,1.4,i/(len(trans)-1)))]; // sweep(shape, concat(outside,inside),closed=true); +// Example: Using path_sweep on a region +// rgn1 = [for (d=[10:10:60]) circle(d=d,$fn=8)]; +// rgn2 = [square(30,center=false)]; +// rgn3 = [for (size=[10:10:20]) move([15,15],p=square(size=size, center=true))]; +// mrgn = union(rgn1,rgn2); +// orgn = difference(mrgn,rgn3); +// path_sweep(orgn,arc(r=40,angle=180)); +// Example: A region with a twist +// region = [for(i=pentagon(5)) move(i,p=circle(r=2,$fn=25))]; +// path_sweep(region, +// circle(r=16,$fn=75),closed=true, +// twist=360/5*2,symmetry=5); module path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10, anchor="origin",cp,spin=0, orient=UP, extent=false) @@ -1225,7 +1260,7 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi assert(!closed || twist % (360/symmetry)==0, str("For a closed sweep, twist must be a multiple of 360/symmetry = ",360/symmetry)) assert(closed || symmetry==1, "symmetry must be 1 when closed is false") assert(is_integer(symmetry) && symmetry>0, "symmetry must be a positive integer") - assert(is_path(shape,2) || is_region(shape), "shape must be a 2d path or region.") +// let(shape = check_and_fix_path(shape,valid_dim=2,closed=true,name="shape")) assert(is_path(path), "input path is not a path") assert(!closed || !approx(path[0],select(path,-1)), "Closed path includes start point at the end") let( @@ -1301,15 +1336,15 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi translate(path[i%L])*rotation*zrot(-twist*pathfrac[i]) ] : assert(false,"Unknown method or no method given")[], // unknown method - ends_match = !closed ? true : - let( - start = apply(transform_list[0],path3d(shape)), - end = reindex_polygon(start, apply(transform_list[L],path3d(shape))) - ) - all([for(i=idx(start)) approx(start[i],end[i])]), + ends_match = !closed ? true + : let( rshape = is_path(shape) ? [path3d(shape)] + : [for(s=shape) path3d(s)] + ) + regions_equal(apply(transform_list[0], rshape), + apply(transform_list[L], rshape)), dummy = ends_match ? 0 : echo("WARNING: ***** The points do not match when closing the model *****") ) - transforms ? transform_list : sweep(clockwise_polygon(shape), transform_list, closed=false, caps=fullcaps); + transforms ? transform_list : sweep(is_path(shape)?clockwise_polygon(shape):shape, transform_list, closed=false, caps=fullcaps); // Function&Module: path_sweep2d() @@ -1361,7 +1396,8 @@ function path_sweep2d(shape, path, closed=false, caps, quality=1) = caps = is_def(caps) ? caps : closed ? false : true, capsOK = is_bool(caps) || (is_list(caps) && len(caps)==2 && is_bool(caps[0]) && is_bool(caps[1])), - fullcaps = is_bool(caps) ? [caps,caps] : caps + fullcaps = is_bool(caps) ? [caps,caps] : caps, + shape = check_and_fix_path(shape,valid_dim=2,closed=true,name="shape") ) assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps")