diff --git a/affine.scad b/affine.scad index b1a328a..07c662c 100644 --- a/affine.scad +++ b/affine.scad @@ -50,7 +50,7 @@ function ident(n) = [ // Arguments: // x = The value to test for being an affine matrix. // dim = The number of dimensions the given affine is required to be for. Generally 2 for 2D or 3 for 3D. If given as a list of integers, allows any of the given dimensions. Default: `[2,3]` -// Examples: +// Example: // bool = is_affine(affine2d_scale([2,3])); // Returns true // bool = is_affine(affine3d_scale([2,3,4])); // Returns true // bool = is_affine(affine3d_scale([2,3,4]),2); // Returns false @@ -74,7 +74,7 @@ function is_affine(x,dim=[2,3]) = // for a simple scaling of z. Note that an input which is only a zscale returns false. // Arguments: // t = The transformation matrix to check. -// Examples: +// Example: // b = is_2d_transform(zrot(45)); // Returns: true // b = is_2d_transform(yrot(45)); // Returns: false // b = is_2d_transform(xrot(45)); // Returns: false diff --git a/fnliterals.scad b/fnliterals.scad index f5f5951..c0cde8b 100644 --- a/fnliterals.scad +++ b/fnliterals.scad @@ -141,12 +141,10 @@ function reduce(func, list, init=0) = // list = The input list. // init = The starting value for the accumulator. Default: 0 // See Also: map(), filter(), reduce(), while(), for_n() -// Examples: Reimplement cumsum() -// echo(accumulate(function (a,b) a+b, [3,4,5],0)); -// // ECHO: [3,7,12] -// Examples: Reimplement cumprod() -// echo(accumulate(f_mul(),[3,4,5],1)); -// // ECHO: [3,12,60,360] +// Example: Reimplement cumsum() +// echo(accumulate(function (a,b) a+b, [3,4,5],0)); // ECHO: [3,7,12] +// Example: Reimplement cumprod() +// echo(accumulate(f_mul(),[3,4,5],1)); // ECHO: [3,12,60,360] function accumulate(func, list, init=0) = assert(is_function(func)) assert(is_list(list)) @@ -313,7 +311,7 @@ function binsearch(key, list, idx, cmp=f_cmp()) = // Arguments: // x = The value to get the simple hash value of. // See Also: hashmap() -// Examples: +// Example: // x = simple_hash("Foobar"); // x = simple_hash([[10,20],[-5,3]]); function simple_hash(x) = diff --git a/gears.scad b/gears.scad index f44acd7..58a9122 100644 --- a/gears.scad +++ b/gears.scad @@ -37,7 +37,7 @@ // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // mod = The metric module/modulus of the gear. -// Examples: +// Example: // circp = circular_pitch(pitch=5); // circp = circular_pitch(mod=2); function circular_pitch(pitch=5, mod) = @@ -54,7 +54,7 @@ function circular_pitch(pitch=5, mod) = // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // mod = The metric module/modulus of the gear. -// Examples: +// Example: // dp = diametral_pitch(pitch=5); // dp = diametral_pitch(mod=2); function diametral_pitch(pitch=5, mod) = @@ -96,7 +96,7 @@ function module_value(pitch=5) = pitch / PI; // Arguments: // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // mod = The metric module/modulus of the gear. -// Examples: +// Example: // ad = adendum(pitch=5); // ad = adendum(mod=2); // Example(2D): @@ -123,7 +123,7 @@ function adendum(pitch=5, mod) = // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // clearance = If given, sets the clearance between meshing teeth. // mod = The metric module/modulus of the gear. -// Examples: +// Example: // ddn = dedendum(pitch=5); // ddn = dedendum(mod=2); // Example(2D): @@ -152,7 +152,7 @@ function dedendum(pitch=5, clearance, mod) = // pitch = The circular pitch, or distance between teeth around the pitch circle, in mm. // teeth = The number of teeth on the gear. // mod = The metric module/modulus of the gear. -// Examples: +// Example: // pr = pitch_radius(pitch=5, teeth=11); // pr = pitch_radius(mod=2, teeth=20); // Example(2D): @@ -177,7 +177,7 @@ function pitch_radius(pitch=5, teeth=11, mod) = // clearance = If given, sets the clearance between meshing teeth. // interior = If true, calculate for an interior gear. // mod = The metric module/modulus of the gear. -// Examples: +// Example: // or = outer_radius(pitch=5, teeth=20); // or = outer_radius(mod=2, teeth=16); // Example(2D): @@ -203,7 +203,7 @@ function outer_radius(pitch=5, teeth=11, clearance, interior=false, mod) = // clearance = If given, sets the clearance between meshing teeth. // interior = If true, calculate for an interior gear. // mod = The metric module/modulus of the gear. -// Examples: +// Example: // rr = root_radius(pitch=5, teeth=11); // rr = root_radius(mod=2, teeth=16); // Example(2D): @@ -228,7 +228,7 @@ function root_radius(pitch=5, teeth=11, clearance, interior=false, mod) = // teeth = The number of teeth on the gear. // pressure_angle = Pressure angle in degrees. Controls how straight or bulged the tooth sides are. // mod = The metric module/modulus of the gear. -// Examples: +// Example: // br = base_radius(pitch=5, teeth=20, pressure_angle=20); // br = base_radius(mod=2, teeth=18, pressure_angle=20); // Example(2D): @@ -253,7 +253,7 @@ function base_radius(pitch=5, teeth=11, pressure_angle=28, mod) = // teeth = Number of teeth that this gear has. // mate_teeth = Number of teeth that the matching gear has. // drive_angle = Angle between the drive shafts of each gear. Default: 90ยบ. -// Examples: +// Example: // ang = bevel_pitch_angle(teeth=18, mate_teeth=30); // Example(2D): // t1 = 13; t2 = 19; pitch=5; @@ -287,7 +287,7 @@ function bevel_pitch_angle(teeth, mate_teeth, drive_angle=90) = // crowning = The amount to oversize the virtual hobbing cutter used to make the teeth, to add a slight crowning to the teeth to make them fir the work easier. Default: 1 // clearance = Clearance gap at the bottom of the inter-tooth valleys. // mod = The metric module/modulus of the gear. -// Examples: +// Example: // thick = worm_gear_thickness(pitch=5, teeth=36, worm_diam=30); // thick = worm_gear_thickness(mod=2, teeth=28, worm_diam=25); // Example(2D): diff --git a/geometry.scad b/geometry.scad index 1dde87c..514d4e4 100644 --- a/geometry.scad +++ b/geometry.scad @@ -113,7 +113,7 @@ function point_line_distance(pt, line, bounded=false) = // Function: segment_distance() // Usage: -// dist = segment_distance(seg1, seg2); +// dist = segment_distance(seg1, seg2, [eps]); // Topics: Geometry, Segments, Distance // See Also: convex_collision(), convex_distance() // Description: @@ -121,12 +121,13 @@ function point_line_distance(pt, line, bounded=false) = // Arguments: // seg1 = The list of two points representing the first line segment to check the distance of. // seg2 = The list of two points representing the second line segment to check the distance of. +// eps = tolerance for point comparisons // Example: // dist = segment_distance([[-14,3], [-15,9]], [[-10,0], [10,0]]); // Returns: 5 // dist2 = segment_distance([[-5,5], [5,-5]], [[-10,3], [10,-3]]); // Returns: 0 -function segment_distance(seg1, seg2) = +function segment_distance(seg1, seg2,eps=EPSILON) = assert( is_matrix(concat(seg1,seg2),4), "Inputs should be two valid segments." ) - convex_distance(seg1,seg2); + convex_distance(seg1,seg2,eps); // Function: line_normal() @@ -1476,7 +1477,7 @@ function polygon_normal(poly) = // color("red")back(28/(2/3))text("Even-Odd", size=5/(2/3), halign="center"); // } // right(40){ -// dp = decompose_path(path,closed=true); +// dp = polygon_parts(path,closed=true); // region(dp); // color("red"){stroke(path,width=1,closed=true); // back(28/(2/3))text("Nonzero", size=5/(2/3), halign="center"); diff --git a/metric_screws.scad b/metric_screws.scad index c460d79..36a749d 100644 --- a/metric_screws.scad +++ b/metric_screws.scad @@ -8,8 +8,7 @@ include -include -include +include // Section: Functions @@ -607,13 +606,13 @@ module metric_bolt( // Phillips drive hole if (headtype != "socket" && phillips != undef) { down(headtype != "hex"? H/6 : 0) { - phillips_drive(size=phillips, shaft=D); + phillips_mask(size=phillips); //, shaft=D); } } // Torx drive hole if (headtype != "socket" && torx != undef) { - up(1) torx_drive(size=torx, l=H+0.1, center=false); + up(1) torx_mask(size=torx, l=H+0.1, center=false); } } } diff --git a/mutators.scad b/mutators.scad index 0f51e73..ebc8f68 100644 --- a/mutators.scad +++ b/mutators.scad @@ -1036,6 +1036,7 @@ module HSV(h,s=1,v=1,a=1) color(HSV(h,s,v),a) children(); // Arguments: // list = The list of items to iterate through. // stride = Consecutive colors stride around the color wheel divided into this many parts. +// maxhues = max number of hues to use (to prevent lots of indistinguishable hues) // Side Effects: // Sets the color to progressive values along the ROYGBIV spectrum for each item. // Sets `$idx` to the index of the current item in `list` that we want to show. @@ -1045,11 +1046,14 @@ module HSV(h,s=1,v=1,a=1) color(HSV(h,s,v),a) children(); // Example(2D): // rgn = [circle(d=45,$fn=3), circle(d=75,$fn=4), circle(d=50)]; // rainbow(rgn) stroke($item, closed=true); -module rainbow(list, stride=1) +module rainbow(list, stride=1, maxhues) { ll = len(list); - huestep = 360 / ll; + maxhues = first_defined([maxhues,ll]); + huestep = 360 / maxhues; hues = [for (i=[0:1:ll-1]) posmod(i*huestep+i*360/stride,360)]; + echo(hues=hues); + s = [for (i=[0:1:ll-1]) [.5,.7,1][posmod(i,3)]]; for($idx=idx(list)) { $item = list[$idx]; HSV(h=hues[$idx]) children(); diff --git a/paths.scad b/paths.scad index 83b301b..462fa28 100644 --- a/paths.scad +++ b/paths.scad @@ -6,7 +6,7 @@ ////////////////////////////////////////////////////////////////////// -// Section: Functions +// Section: Utility Functions // Function: is_path() @@ -72,7 +72,7 @@ function cleanup_path(path, eps=EPSILON) = is_closed_path(path,eps=eps)? [for (i=[0:1:len(path)-2]) path[i]] : path; -/// internal Function: _path_select() +/// Internal Function: _path_select() /// Usage: /// _path_select(path,s1,u1,s2,u2,[closed]): /// Description: @@ -109,21 +109,24 @@ function _path_select(path, s1, u1, s2, u2, closed=false) = // path_merge_collinear(path, [eps]) // Arguments: // path = A list of path points of any dimension. +// closed = treat as closed polygon. Default: false // eps = Largest positional variance allowed. Default: `EPSILON` (1-e9) -function path_merge_collinear(path, eps=EPSILON) = +function path_merge_collinear(path, closed=false, eps=EPSILON) = assert( is_path(path), "Invalid path." ) assert( is_undef(eps) || (is_finite(eps) && (eps>=0) ), "Invalid tolerance." ) len(path)<=2 ? path : let( indices = [ 0, - for (i=[1:1:len(path)-2]) - if (!is_collinear(path[i-1], path[i], path[i+1], eps=eps)) i, - len(path)-1 + for (i=[1:1:len(path)-(closed?1:2)]) + if (!is_collinear(path[i-1], path[i], select(path,i+1), eps=eps)) i, + if (!closed) len(path)-1 ] ) [for (i=indices) path[i]]; +// Section: Path length calculation + // Function: path_length() // Usage: @@ -156,6 +159,286 @@ function path_segment_lengths(path, closed=false) = ]; +// Function: path_length_fractions() +// Usage: +// fracs = path_length_fractions(path, [closed]); +// Description: +// Returns the distance fraction of each point in the path along the path, so the first +// point is zero and the final point is 1. If the path is closed the length of the output +// will have one extra point because of the final connecting segment that connects the last +// point of the path to the first point. +function path_length_fractions(path, closed=false) = + assert(is_path(path)) + assert(is_bool(closed)) + let( + lengths = [ + 0, + for (i=[0:1:len(path)-(closed?1:2)]) + norm(select(path,i+1)-path[i]) + ], + partial_len = cumsum(lengths), + total_len = last(partial_len) + ) partial_len / total_len; + + + +/// Internal Function: _path_self_intersections() +/// Usage: +/// isects = _path_self_intersections(path, [closed], [eps]); +/// Description: +/// Locates all self intersections of the given path. Returns a list of intersections, where +/// each intersection is a list like [POINT, SEGNUM1, PROPORTION1, SEGNUM2, PROPORTION2] where +/// POINT is the coordinates of the intersection point, SEGNUMs are the integer indices of the +/// intersecting segments along the path, and the PROPORTIONS are the 0.0 to 1.0 proportions +/// of how far along those segments they intersect at. A proportion of 0.0 indicates the start +/// of the segment, and a proportion of 1.0 indicates the end of the segment. +/// Arguments: +/// path = The path to find self intersections of. +/// closed = If true, treat path like a closed polygon. Default: true +/// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9) +/// Example(2D): +/// path = [ +/// [-100,100], [0,-50], [100,100], [100,-100], [0,50], [-100,-100] +/// ]; +/// isects = _path_self_intersections(path, closed=true); +/// // isects == [[[-33.3333, 0], 0, 0.666667, 4, 0.333333], [[33.3333, 0], 1, 0.333333, 3, 0.666667]] +/// stroke(path, closed=true, width=1); +/// for (isect=isects) translate(isect[0]) color("blue") sphere(d=10); +function _path_self_intersections(path, closed=true, eps=EPSILON) = + let( + path = cleanup_path(path, eps=eps), + plen = len(path) + ) [ + for (i = [0:1:plen-(closed?2:3)], j=[i+2:1:plen-(closed?1:2)]) let( + a1 = path[i], + a2 = path[(i+1)%plen], + b1 = path[j], + b2 = path[(j+1)%plen], + isect = + (max(a1.x, a2.x) < min(b1.x, b2.x))? undef : + (min(a1.x, a2.x) > max(b1.x, b2.x))? undef : + (max(a1.y, a2.y) < min(b1.y, b2.y))? undef : + (min(a1.y, a2.y) > max(b1.y, b2.y))? undef : + let( + c = a1-a2, + d = b1-b2, + denom = (c.x*d.y)-(c.y*d.x) + ) abs(denom)=-eps && isect[1]<=1+eps && + isect[2]>=-eps && isect[2]<=1+eps + ) [isect[0], i, isect[1], j, isect[2]] + ]; + + + +// Section: Resampling: changing the number of points in a path + + +// Input `data` is a list that sums to an integer. +// Returns rounded version of input data so that every +// entry is rounded to an integer and the sum is the same as +// that of the input. Works by rounding an entry in the list +// and passing the rounding error forward to the next entry. +// This will generally distribute the error in a uniform manner. +function _sum_preserving_round(data, index=0) = + index == len(data)-1 ? list_set(data, len(data)-1, round(data[len(data)-1])) : + let( + newval = round(data[index]), + error = newval - data[index] + ) _sum_preserving_round( + list_set(data, [index,index+1], [newval, data[index+1]-error]), + index+1 + ); + + +// Function: subdivide_path() +// Usage: +// newpath = subdivide_path(path, [N|refine], method); +// Description: +// Takes a path as input (closed or open) and subdivides the path to produce a more +// finely sampled path. The new points can be distributed proportional to length +// (`method="length"`) or they can be divided up evenly among all the path segments +// (`method="segment"`). If the extra points don't fit evenly on the path then the +// algorithm attempts to distribute them uniformly. The `exact` option requires that +// the final length is exactly as requested. If you set it to `false` then the +// algorithm will favor uniformity and the output path may have a different number of +// points due to rounding error. +// . +// With the `"segment"` method you can also specify a vector of lengths. This vector, +// `N` specfies the desired point count on each segment: with vector input, `subdivide_path` +// attempts to place `N[i]-1` points on segment `i`. The reason for the -1 is to avoid +// double counting the endpoints, which are shared by pairs of segments, so that for +// a closed polygon the total number of points will be sum(N). Note that with an open +// path there is an extra point at the end, so the number of points will be sum(N)+1. +// Arguments: +// path = path to subdivide +// N = scalar total number of points desired or with `method="segment"` can be a vector requesting `N[i]-1` points on segment i. +// refine = number of points to add each segment. +// closed = set to false if the path is open. Default: True +// exact = if true return exactly the requested number of points, possibly sacrificing uniformity. If false, return uniform point sample that may not match the number of points requested. Default: True +// method = One of `"length"` or `"segment"`. If `"length"`, adds vertices evenly along the total path length. If `"segment"`, adds points evenly among the segments. Default: `"length"` +// Example(2D): +// mypath = subdivide_path(square([2,2],center=true), 12); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): +// mypath = subdivide_path(square([8,2],center=true), 12); +// move_copies(mypath)circle(r=.2,$fn=32); +// Example(2D): +// mypath = subdivide_path(square([8,2],center=true), 12, method="segment"); +// move_copies(mypath)circle(r=.2,$fn=32); +// Example(2D): +// mypath = subdivide_path(square([2,2],center=true), 17, closed=false); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): Specifying different numbers of points on each segment +// mypath = subdivide_path(hexagon(side=2), [2,3,4,5,6,7], method="segment"); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): Requested point total is 14 but 15 points output due to extra end point +// mypath = subdivide_path(pentagon(side=2), [3,4,3,4], method="segment", closed=false); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): Since 17 is not divisible by 5, a completely uniform distribution is not possible. +// mypath = subdivide_path(pentagon(side=2), 17); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): With `exact=false` a uniform distribution, but only 15 points +// mypath = subdivide_path(pentagon(side=2), 17, exact=false); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): With `exact=false` you can also get extra points, here 20 instead of requested 18 +// mypath = subdivide_path(pentagon(side=2), 18, exact=false); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(FlatSpin,VPD=15,VPT=[0,0,1.5]): Three-dimensional paths also work +// mypath = subdivide_path([[0,0,0],[2,0,1],[2,3,2]], 12); +// move_copies(mypath)sphere(r=.1,$fn=32); +function subdivide_path(path, N, refine, closed=true, exact=true, method="length") = + assert(is_path(path)) + assert(method=="length" || method=="segment") + assert(num_defined([N,refine]),"Must give exactly one of N and refine") + let( + N = !is_undef(N)? N : + !is_undef(refine)? len(path) * refine : + undef + ) + assert((is_num(N) && N>0) || is_vector(N),"Parameter N to subdivide_path must be postive number or vector") + let( + count = len(path) - (closed?0:1), + add_guess = method=="segment"? ( + is_list(N)? ( + assert(len(N)==count,"Vector parameter N to subdivide_path has the wrong length") + add_scalar(N,-1) + ) : repeat((N-len(path)) / count, count) + ) : // method=="length" + assert(is_num(N),"Parameter N to subdivide path must be a number when method=\"length\"") + let( + path_lens = concat( + [ for (i = [0:1:len(path)-2]) norm(path[i+1]-path[i]) ], + closed? [norm(path[len(path)-1]-path[0])] : [] + ), + add_density = (N - len(path)) / sum(path_lens) + ) + path_lens * add_density, + add = exact? _sum_preserving_round(add_guess) : + [for (val=add_guess) round(val)] + ) concat( + [ + for (i=[0:1:count]) each [ + for(j=[0:1:add[i]]) + lerp(path[i],select(path,i+1), j/(add[i]+1)) + ] + ], + closed? [] : [last(path)] + ); + + + +// Function: subdivide_long_segments() +// Topics: Paths, Path Subdivision +// See Also: subdivide_path(), subdivide_and_slice(), path_add_jitter(), jittered_poly() +// Usage: +// spath = subdivide_long_segments(path, maxlen, [closed=]); +// Description: +// Evenly subdivides long `path` segments until they are all shorter than `maxlen`. +// Arguments: +// path = The path to subdivide. +// maxlen = The maximum allowed path segment length. +// --- +// closed = If true, treat path like a closed polygon. Default: true +// Example: +// path = pentagon(d=100); +// spath = subdivide_long_segments(path, 10, closed=true); +// stroke(path); +// color("lightgreen") move_copies(path) circle(d=5,$fn=12); +// color("blue") move_copies(spath) circle(d=3,$fn=12); +function subdivide_long_segments(path, maxlen, closed=false) = + assert(is_path(path)) + assert(is_finite(maxlen)) + assert(is_bool(closed)) + [ + for (p=pair(path,closed)) let( + steps = ceil(norm(p[1]-p[0])/maxlen) + ) each lerpn(p[0], p[1], steps, false), + if (!closed) last(path) + ]; + + + +// Function: resample_path() +// Usage: +// newpath = resample_path(path, N|spacing, [closed]); +// Description: +// Compute a uniform resampling of the input path. If you specify `N` then the output path will have N +// points spaced uniformly (by linear interpolation along the input path segments). The only points of the +// input path that are guaranteed to appear in the output path are the starting and ending points. +// If you specify `spacing` then the length you give will be rounded to the nearest spacing that gives +// a uniform sampling of the path and the resulting uniformly sampled path is returned. +// Note that because this function operates on a discrete input path the quality of the output depends on +// the sampling of the input. If you want very accurate output, use a lot of points for the input. +// Arguments: +// path = path to resample +// N = Number of points in output +// spacing = Approximate spacing desired +// closed = set to true if path is closed. Default: false +function resample_path(path, N, spacing, closed=false) = + assert(is_path(path)) + assert(num_defined([N,spacing])==1,"Must define exactly one of N and spacing") + assert(is_bool(closed)) + let( + length = path_length(path,closed), + // In the open path case decrease N by 1 so that we don't try to get + // path_cut to return the endpoint (which might fail due to rounding) + // Add last point later + N = is_def(N) ? N-(closed?0:1) : round(length/spacing), + distlist = lerpn(0,length,N,false), + cuts = _path_cut_points(path, distlist, closed=closed) + ) + [ each subindex(cuts,0), + if (!closed) last(path) // Then add last point here + ]; + + + + + +// Section: Path Geometry + +// Function: is_path_simple() +// Usage: +// bool = is_path_simple(path, [closed], [eps]); +// Description: +// Returns true if the path is simple, meaning that it has no self-intersections. +// If closed is set to true then treat the path as a polygon. +// Arguments: +// path = path to check +// closed = set to true to treat path as a polygon. Default: false +// eps = Epsilon error value used for determine if points coincide. Default: `EPSILON` (1e-9) +function is_path_simple(path, closed=false, eps=EPSILON) = + _path_self_intersections(path,closed=closed,eps=eps) == []; + // Function: path_closest_point() // Usage: @@ -286,6 +569,8 @@ function path_torsion(path, closed=false) = ]; +// Section: Modifying paths + // Function: path_chamfer_and_rounding() // Usage: // path2 = path_chamfer_and_rounding(path, [closed], [chamfer], [rounding]); @@ -438,97 +723,201 @@ function _corner_roundover_path(p1, p2, p3, r, d) = -// Function: path_add_jitter() -// Topics: Paths -// See Also: jittered_poly(), subdivide_long_segments() -// Usage: -// jpath = path_add_jitter(path, [dist], [closed=]); -// Description: -// Adds tiny jitter offsets to collinear points in the given path so that they -// are no longer collinear. This is useful for preserving subdivision on long -// straight segments, when making geometry with `polygon()`, for use with -// `linear_exrtrude()` with a `twist()`. -// Arguments: -// path = The path to add jitter to. -// dist = The amount to jitter points by. Default: 1/512 (0.00195) -// --- -// closed = If true, treat path like a closed polygon. Default: true -// Example(3D): -// d = 100; h = 75; quadsize = 5; -// path = pentagon(d=d); -// spath = subdivide_long_segments(path, quadsize, closed=true); -// jpath = path_add_jitter(spath, closed=true); -// linear_extrude(height=h, twist=72, slices=h/quadsize) -// polygon(jpath); -function path_add_jitter(path, dist=1/512, closed=true) = - assert(is_path(path)) - assert(is_finite(dist)) - assert(is_bool(closed)) - [ - path[0], - for (i=idx(path,s=1,e=closed?-1:-2)) let( - n = line_normal([path[i-1],path[i]]) - ) path[i] + n * (is_collinear(select(path,i-1,i+1))? (dist * ((i%2)*2-1)) : 0), - if (!closed) last(path) - ]; + +// Section: Breaking paths up into subpaths +/// Internal Function: _path_cut_points() +/// +/// Usage: +/// cuts = _path_cut_points(path, dists, [closed=], [direction=]); +/// +/// Description: +/// Cuts a path at a list of distances from the first point in the path. Returns a list of the cut +/// points and indices of the next point in the path after that point. So for example, a return +/// value entry of [[2,3], 5] means that the cut point was [2,3] and the next point on the path after +/// this point is path[5]. If the path is too short then _path_cut_points returns undef. If you set +/// `direction` to true then `_path_cut_points` will also return the tangent vector to the path and a normal +/// vector to the path. It tries to find a normal vector that is coplanar to the path near the cut +/// point. If this fails it will return a normal vector parallel to the xy plane. The output with +/// direction vectors will be `[point, next_index, tangent, normal]`. +/// . +/// If you give the very last point of the path as a cut point then the returned index will be +/// one larger than the last index (so it will not be a valid index). If you use the closed +/// option then the returned index will be equal to the path length for cuts along the closing +/// path segment, and if you give a point equal to the path length you will get an +/// index of len(path)+1 for the index. +/// +/// Arguments: +/// path = path to cut +/// dists = distances where the path should be cut (a list) or a scalar single distance +/// --- +/// closed = set to true if the curve is closed. Default: false +/// direction = set to true to return direction vectors. Default: false +/// +/// Example(NORENDER): +/// square=[[0,0],[1,0],[1,1],[0,1]]; +/// _path_cut_points(square, [.5,1.5,2.5]); // Returns [[[0.5, 0], 1], [[1, 0.5], 2], [[0.5, 1], 3]] +/// _path_cut_points(square, [0,1,2,3]); // Returns [[[0, 0], 1], [[1, 0], 2], [[1, 1], 3], [[0, 1], 4]] +/// _path_cut_points(square, [0,0.8,1.6,2.4,3.2], closed=true); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], [[0, 0.8], 4]] +/// _path_cut_points(square, [0,0.8,1.6,2.4,3.2]); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], undef] +function _path_cut_points(path, dists, closed=false, direction=false) = + let(long_enough = len(path) >= (closed ? 3 : 2)) + assert(long_enough,len(path)<2 ? "Two points needed to define a path" : "Closed path must include three points") + is_num(dists) ? _path_cut_points(path, [dists],closed, direction)[0] : + assert(is_vector(dists)) + assert(list_increasing(dists), "Cut distances must be an increasing list") + let(cuts = _path_cut_points_recurse(path,dists,closed)) + !direction + ? cuts + : let( + dir = _path_cuts_dir(path, cuts, closed), + normals = _path_cuts_normals(path, cuts, dir, closed) + ) + hstack(cuts, array_group(dir,1), array_group(normals,1)); -// Function: path_self_intersections() -// Usage: -// isects = path_self_intersections(path, [eps]); -// Description: -// Locates all self intersections of the given path. Returns a list of intersections, where -// each intersection is a list like [POINT, SEGNUM1, PROPORTION1, SEGNUM2, PROPORTION2] where -// POINT is the coordinates of the intersection point, SEGNUMs are the integer indices of the -// intersecting segments along the path, and the PROPORTIONS are the 0.0 to 1.0 proportions -// of how far along those segments they intersect at. A proportion of 0.0 indicates the start -// of the segment, and a proportion of 1.0 indicates the end of the segment. -// Arguments: -// path = The path to find self intersections of. -// closed = If true, treat path like a closed polygon. Default: true -// eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9) -// Example(2D): -// path = [ -// [-100,100], [0,-50], [100,100], [100,-100], [0,50], [-100,-100] -// ]; -// isects = path_self_intersections(path, closed=true); -// // isects == [[[-33.3333, 0], 0, 0.666667, 4, 0.333333], [[33.3333, 0], 1, 0.333333, 3, 0.666667]] -// stroke(path, closed=true, width=1); -// for (isect=isects) translate(isect[0]) color("blue") sphere(d=10); -function path_self_intersections(path, closed=true, eps=EPSILON) = +// Main recursive path cut function +function _path_cut_points_recurse(path, dists, closed=false, pind=0, dtotal=0, dind=0, result=[]) = + dind == len(dists) ? result : let( - path = cleanup_path(path, eps=eps), - plen = len(path) - ) [ - for (i = [0:1:plen-(closed?2:3)], j=[i+2:1:plen-(closed?1:2)]) let( - a1 = path[i], - a2 = path[(i+1)%plen], - b1 = path[j], - b2 = path[(j+1)%plen], - isect = - (max(a1.x, a2.x) < min(b1.x, b2.x))? undef : - (min(a1.x, a2.x) > max(b1.x, b2.x))? undef : - (max(a1.y, a2.y) < min(b1.y, b2.y))? undef : - (min(a1.y, a2.y) > max(b1.y, b2.y))? undef : - let( - c = a1-a2, - d = b1-b2, - denom = (c.x*d.y)-(c.y*d.x) - ) abs(denom)=-eps && isect[1]<=1+eps && - isect[2]>=-eps && isect[2]<=1+eps - ) [isect[0], i, isect[1], j, isect[2]] + lastpt = len(result)==0? [] : last(result)[0], // location of last cut point + dpartial = len(result)==0? 0 : norm(lastpt-select(path,pind)), // remaining length in segment + nextpoint = dists[dind] < dpartial+dtotal // Do we have enough length left on the current segment? + ? [lerp(lastpt,select(path,pind),(dists[dind]-dtotal)/dpartial),pind] + : _path_cut_single(path, dists[dind]-dtotal-dpartial, closed, pind) + ) + _path_cut_points_recurse(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint])); + + +// Search for a single cut point in the path +function _path_cut_single(path, dist, closed=false, ind=0, eps=1e-7) = + // If we get to the very end of the path (ind is last point or wraparound for closed case) then + // check if we are within epsilon of the final path point. If not we're out of path, so we fail + ind==len(path)-(closed?0:1) ? + assert(dist dist ? + [lerp(path[ind],select(path,ind+1),dist/d), ind+1] : + _path_cut_single(path, dist-d,closed, ind+1, eps); + +// Find normal directions to the path, coplanar to local part of the path +// Or return a vector parallel to the x-y plane if the above fails +function _path_cuts_normals(path, cuts, dirs, closed=false) = + [for(i=[0:len(cuts)-1]) + len(path[0])==2? [-dirs[i].y, dirs[i].x] + : + let( + plane = len(path)<3 ? undef : + let(start = max(min(cuts[i][1],len(path)-1),2)) _path_plane(path, start, start-2) + ) + plane==undef? + ( dirs[i].x==0 && dirs[i].y==0 ? [1,0,0] // If it's z direction return x vector + : unit([-dirs[i].y, dirs[i].x,0])) // otherwise perpendicular to projection + : unit(cross(dirs[i],cross(plane[0],plane[1]))) ]; +// Scan from the specified point (ind) to find a noncoplanar triple to use +// to define the plane of the path. +function _path_plane(path, ind, i,closed) = + i<(closed?-1:0) ? undef : + !is_collinear(path[ind],path[ind-1], select(path,i))? + [select(path,i)-path[ind-1],path[ind]-path[ind-1]] : + _path_plane(path, ind, i-1); + +// Find the direction of the path at the cut points +function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) = + [for(ind=[0:len(cuts)-1]) + let( + zeros = path[0]*0, + nextind = cuts[ind][1], + nextpath = unit(select(path, nextind+1)-select(path, nextind),zeros), + thispath = unit(select(path, nextind) - select(path,nextind-1),zeros), + lastpath = unit(select(path,nextind-1) - select(path, nextind-2),zeros), + nextdir = + nextind==len(path) && !closed? lastpath : + (nextind<=len(path)-2 || closed) && approx(cuts[ind][0], path[nextind],eps) + ? unit(nextpath+thispath) + : (nextind>1 || closed) && approx(cuts[ind][0],select(path,nextind-1),eps) + ? unit(thispath+lastpath) + : thispath + ) nextdir + ]; + + +// Function: path_cut() +// Topics: Paths +// See Also: split_path_at_self_crossings() +// Usage: +// path_list = path_cut(path, cutdist, [closed=]); +// Description: +// Given a list of distances in `cutdist`, cut the path into +// subpaths at those lengths, returning a list of paths. +// If the input path is closed then the final path will include the +// original starting point. The list of cut distances must be +// in ascending order and should not include the endpoints: 0 +// or len(path). If you repeat a distance you will get an +// empty list in that position in the output. If you give an +// empty cutdist array you will get the input path as output +// (without the final vertex doubled in the case of a closed path). +// Arguments: +// path = The original path to split. +// cutdist = Distance or list of distances where path is cut +// closed = If true, treat the path as a closed polygon. +// Example(2D): +// path = circle(d=100); +// segs = path_cut(path, [50, 200], closed=true); +// rainbow(segs) stroke($item); +function path_cut(path,cutdist,closed) = + is_num(cutdist) ? path_cut(path,[cutdist],closed) : + assert(is_vector(cutdist)) + assert(last(cutdist)0, "Cut distances must be strictly positive") + let( + cutlist = _path_cut_points(path,cutdist,closed=closed) + ) + _path_cut_getpaths(path, cutlist, closed); + + +function _path_cut_getpaths(path, cutlist, closed) = + let( + cuts = len(cutlist) + ) + [ + [ each list_head(path,cutlist[0][1]-1), + if (!approx(cutlist[0][0], path[cutlist[0][1]-1])) cutlist[0][0] + ], + for(i=[0:1:cuts-2]) + cutlist[i][0]==cutlist[i+1][0] && cutlist[i][1]==cutlist[i+1][1] ? [] + : + [ if (!approx(cutlist[i][0], select(path,cutlist[i][1]))) cutlist[i][0], + each slice(path, cutlist[i][1], cutlist[i+1][1]-1), + if (!approx(cutlist[i+1][0], select(path,cutlist[i+1][1]-1))) cutlist[i+1][0], + ], + [ + if (!approx(cutlist[cuts-1][0], select(path,cutlist[cuts-1][1]))) cutlist[cuts-1][0], + each select(path,cutlist[cuts-1][1],closed ? 0 : -1) + ] + ]; + + +// internal function +// converts pathcut output form to a [segment, u] +// form list that works withi path_select +function _cut_to_seg_u_form(pathcut, path, closed) = + let(lastind = len(path) - (closed?0:1)) + [for(entry=pathcut) + entry[1] > lastind ? [lastind,0] : + let( + a = path[entry[1]-1], + b = path[entry[1]], + c = entry[0], + i = max_index(v_abs(b-a)), + factor = (c[i]-a[i])/(b[i]-a[i]) + ) + [entry[1]-1,factor] + ]; + + // Function: split_path_at_self_crossings() // Usage: @@ -553,7 +942,7 @@ function split_path_at_self_crossings(path, closed=true, eps=EPSILON) = [[0, 0]], sort([ for ( - a = path_self_intersections(path, closed=closed, eps=eps), + a = _path_self_intersections(path, closed=closed, eps=eps), ss = [ [a[1],a[2]], [a[3],a[4]] ] ) if (ss[0] != undef) ss ]), @@ -574,10 +963,10 @@ function split_path_at_self_crossings(path, closed=true, eps=EPSILON) = ]; -function _tag_self_crossing_subpaths(path, closed=true, eps=EPSILON) = +function _tag_self_crossing_subpaths(path, nonzero, closed=true, eps=EPSILON) = let( subpaths = split_path_at_self_crossings( - path, closed=closed, eps=eps + path, closed=true, eps=eps ) ) [ for (subpath = subpaths) let( @@ -586,33 +975,46 @@ function _tag_self_crossing_subpaths(path, closed=true, eps=EPSILON) = n = line_normal(seg) / 2048, p1 = mp + n, p2 = mp - n, - p1in = point_in_polygon(p1, path) >= 0, - p2in = point_in_polygon(p2, path) >= 0, + p1in = point_in_polygon(p1, path, nonzero=nonzero) >= 0, + p2in = point_in_polygon(p2, path, nonzero=nonzero) >= 0, tag = (p1in && p2in)? "I" : "O" ) [tag, subpath] ]; -// Function: decompose_path() +// Function: polygon_parts() // Usage: -// splitpaths = decompose_path(path, [closed], [eps]); +// splitpaths = polygon_parts(path, [nonzero], [eps]); // Description: -// Given a possibly self-crossing path, decompose it into non-crossing paths that are on the perimeter -// of the areas bounded by that path. +// Given a possibly self-intersecting polygon, constructs a representation of the original polygon as a list of +// non-intersecting simple polygons. If nonzero is set to true then it uses the nonzero method for defining polygon membership, which +// means it will produce the outer perimeter. // Arguments: // path = The path to split up. -// closed = If true, treat path like a closed polygon. Default: true +// nonzero = If true use the nonzero method for checking if a point is in a polygon. Otherwise use the even-odd method. Default: false // eps = The epsilon error value to determine whether two points coincide. Default: `EPSILON` (1e-9) -// Example(2D): +// Example(2D): This cross-crossing polygon breaks up into its 3 components (regardless of the value of nonzero). // path = [ -// [-100,100], [0,-50], [100,100], [100,-100], [0,50], [-100,-100] +// [-100,100], [0,-50], [100,100], +// [100,-100], [0,50], [-100,-100] // ]; -// splitpaths = decompose_path(path, closed=true); +// splitpaths = polygon_parts(path); // rainbow(splitpaths) stroke($item, closed=true, width=3); -function decompose_path(path, closed=true, eps=EPSILON) = +// Example(2D): With nonzero=false you get even-odd mode which matches OpenSCAD, so the pentagram breaks apart into its five points. +// pentagram = turtle(["move",100,"left",144], repeat=4); +// left(100)polygon(pentagram); +// rainbow(polygon_parts(pentagram,nonzero=false)) +// stroke($item,closed=true); +// Example(2D): With nonzero=true you get only the outer perimeter. You can use this to create the polygon using the nonzero method, which is not supported by OpenSCAD. +// pentagram = turtle(["move",100,"left",144], repeat=4); +// outside = polygon_parts(pentagram,nonzero=true); +// left(100)region(outside); +// rainbow(outside) +// stroke($item,closed=true); +function polygon_parts(path, nonzero=false, closed=true, eps=EPSILON) = let( path = cleanup_path(path, eps=eps), - tagged = _tag_self_crossing_subpaths(path, closed=closed, eps=eps), + tagged = _tag_self_crossing_subpaths(path, nonzero=nonzero, closed=closed, eps=eps), kept = [for (sub = tagged) if(sub[0] == "O") sub[1]], outregion = _assemble_path_fragments(kept, eps=eps) ) outregion; @@ -748,364 +1150,7 @@ function _assemble_path_fragments(fragments, eps=EPSILON, _finished=[]) = -/// Internal Function: _path_cut_points() -/// -/// Usage: -/// cuts = _path_cut_points(path, dists, [closed=], [direction=]); -/// -/// Description: -/// Cuts a path at a list of distances from the first point in the path. Returns a list of the cut -/// points and indices of the next point in the path after that point. So for example, a return -/// value entry of [[2,3], 5] means that the cut point was [2,3] and the next point on the path after -/// this point is path[5]. If the path is too short then _path_cut_points returns undef. If you set -/// `direction` to true then `_path_cut_points` will also return the tangent vector to the path and a normal -/// vector to the path. It tries to find a normal vector that is coplanar to the path near the cut -/// point. If this fails it will return a normal vector parallel to the xy plane. The output with -/// direction vectors will be `[point, next_index, tangent, normal]`. -/// . -/// If you give the very last point of the path as a cut point then the returned index will be -/// one larger than the last index (so it will not be a valid index). If you use the closed -/// option then the returned index will be equal to the path length for cuts along the closing -/// path segment, and if you give a point equal to the path length you will get an -/// index of len(path)+1 for the index. -/// -/// Arguments: -/// path = path to cut -/// dists = distances where the path should be cut (a list) or a scalar single distance -/// --- -/// closed = set to true if the curve is closed. Default: false -/// direction = set to true to return direction vectors. Default: false -/// -/// Example(NORENDER): -/// square=[[0,0],[1,0],[1,1],[0,1]]; -/// _path_cut_points(square, [.5,1.5,2.5]); // Returns [[[0.5, 0], 1], [[1, 0.5], 2], [[0.5, 1], 3]] -/// _path_cut_points(square, [0,1,2,3]); // Returns [[[0, 0], 1], [[1, 0], 2], [[1, 1], 3], [[0, 1], 4]] -/// _path_cut_points(square, [0,0.8,1.6,2.4,3.2], closed=true); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], [[0, 0.8], 4]] -/// _path_cut_points(square, [0,0.8,1.6,2.4,3.2]); // Returns [[[0, 0], 1], [[0.8, 0], 1], [[1, 0.6], 2], [[0.6, 1], 3], undef] -function _path_cut_points(path, dists, closed=false, direction=false) = - let(long_enough = len(path) >= (closed ? 3 : 2)) - assert(long_enough,len(path)<2 ? "Two points needed to define a path" : "Closed path must include three points") - is_num(dists) ? _path_cut_points(path, [dists],closed, direction)[0] : - assert(is_vector(dists)) - assert(list_increasing(dists), "Cut distances must be an increasing list") - let(cuts = _path_cut_points_recurse(path,dists,closed)) - !direction - ? cuts - : let( - dir = _path_cuts_dir(path, cuts, closed), - normals = _path_cuts_normals(path, cuts, dir, closed) - ) - hstack(cuts, array_group(dir,1), array_group(normals,1)); -// Main recursive path cut function -function _path_cut_points_recurse(path, dists, closed=false, pind=0, dtotal=0, dind=0, result=[]) = - dind == len(dists) ? result : - let( - lastpt = len(result)==0? [] : last(result)[0], // location of last cut point - dpartial = len(result)==0? 0 : norm(lastpt-select(path,pind)), // remaining length in segment - nextpoint = dists[dind] < dpartial+dtotal // Do we have enough length left on the current segment? - ? [lerp(lastpt,select(path,pind),(dists[dind]-dtotal)/dpartial),pind] - : _path_cut_single(path, dists[dind]-dtotal-dpartial, closed, pind) - ) - _path_cut_points_recurse(path, dists, closed, nextpoint[1], dists[dind],dind+1, concat(result, [nextpoint])); - - -// Search for a single cut point in the path -function _path_cut_single(path, dist, closed=false, ind=0, eps=1e-7) = - // If we get to the very end of the path (ind is last point or wraparound for closed case) then - // check if we are within epsilon of the final path point. If not we're out of path, so we fail - ind==len(path)-(closed?0:1) ? - assert(dist dist ? - [lerp(path[ind],select(path,ind+1),dist/d), ind+1] : - _path_cut_single(path, dist-d,closed, ind+1, eps); - -// Find normal directions to the path, coplanar to local part of the path -// Or return a vector parallel to the x-y plane if the above fails -function _path_cuts_normals(path, cuts, dirs, closed=false) = - [for(i=[0:len(cuts)-1]) - len(path[0])==2? [-dirs[i].y, dirs[i].x] - : - let( - plane = len(path)<3 ? undef : - let(start = max(min(cuts[i][1],len(path)-1),2)) _path_plane(path, start, start-2) - ) - plane==undef? - ( dirs[i].x==0 && dirs[i].y==0 ? [1,0,0] // If it's z direction return x vector - : unit([-dirs[i].y, dirs[i].x,0])) // otherwise perpendicular to projection - : unit(cross(dirs[i],cross(plane[0],plane[1]))) - ]; - -// Scan from the specified point (ind) to find a noncoplanar triple to use -// to define the plane of the path. -function _path_plane(path, ind, i,closed) = - i<(closed?-1:0) ? undef : - !is_collinear(path[ind],path[ind-1], select(path,i))? - [select(path,i)-path[ind-1],path[ind]-path[ind-1]] : - _path_plane(path, ind, i-1); - -// Find the direction of the path at the cut points -function _path_cuts_dir(path, cuts, closed=false, eps=1e-2) = - [for(ind=[0:len(cuts)-1]) - let( - zeros = path[0]*0, - nextind = cuts[ind][1], - nextpath = unit(select(path, nextind+1)-select(path, nextind),zeros), - thispath = unit(select(path, nextind) - select(path,nextind-1),zeros), - lastpath = unit(select(path,nextind-1) - select(path, nextind-2),zeros), - nextdir = - nextind==len(path) && !closed? lastpath : - (nextind<=len(path)-2 || closed) && approx(cuts[ind][0], path[nextind],eps) - ? unit(nextpath+thispath) - : (nextind>1 || closed) && approx(cuts[ind][0],select(path,nextind-1),eps) - ? unit(thispath+lastpath) - : thispath - ) nextdir - ]; - - -// Function: path_cut() -// Topics: Paths -// Usage: -// path_list = path_cut(path, cutdist, [closed=]); -// Description: -// Given a list of distances in `cutdist`, cut the path into -// subpaths at those lengths, returning a list of paths. -// If the input path is closed then the final path will include the -// original starting point. The list of cut distances must be -// in ascending order and should not include the endpoints: 0 -// or len(path). If you repeat a distance you will get an -// empty list in that position in the output. If you give an -// empty cutdist array you will get the input path as output -// (without the final vertex doubled in the case of a closed path). -// Arguments: -// path = The original path to split. -// cutdist = Distance or list of distances where path is cut -// closed = If true, treat the path as a closed polygon. -// Example(2D): -// path = circle(d=100); -// segs = path_cut(path, [50, 200], closed=true); -// rainbow(segs) stroke($item); -function path_cut(path,cutdist,closed) = - is_num(cutdist) ? path_cut(path,[cutdist],closed) : - assert(is_vector(cutdist)) - assert(last(cutdist)0, "Cut distances must be strictly positive") - let( - cutlist = _path_cut_points(path,cutdist,closed=closed) - ) - _path_cut_getpaths(path, cutlist, closed); - - -function _path_cut_getpaths(path, cutlist, closed) = - let( - cuts = len(cutlist) - ) - [ - [ each list_head(path,cutlist[0][1]-1), - if (!approx(cutlist[0][0], path[cutlist[0][1]-1])) cutlist[0][0] - ], - for(i=[0:1:cuts-2]) - cutlist[i][0]==cutlist[i+1][0] && cutlist[i][1]==cutlist[i+1][1] ? [] - : - [ if (!approx(cutlist[i][0], select(path,cutlist[i][1]))) cutlist[i][0], - each slice(path, cutlist[i][1], cutlist[i+1][1]-1), - if (!approx(cutlist[i+1][0], select(path,cutlist[i+1][1]-1))) cutlist[i+1][0], - ], - [ - if (!approx(cutlist[cuts-1][0], select(path,cutlist[cuts-1][1]))) cutlist[cuts-1][0], - each select(path,cutlist[cuts-1][1],closed ? 0 : -1) - ] - ]; - - -// internal function -// converts pathcut output form to a [segment, u] -// form list that works withi path_select -function _cut_to_seg_u_form(pathcut, path, closed) = - let(lastind = len(path) - (closed?0:1)) - [for(entry=pathcut) - entry[1] > lastind ? [lastind,0] : - let( - a = path[entry[1]-1], - b = path[entry[1]], - c = entry[0], - i = max_index(v_abs(b-a)), - factor = (c[i]-a[i])/(b[i]-a[i]) - ) - [entry[1]-1,factor] - ]; - - - -// Input `data` is a list that sums to an integer. -// Returns rounded version of input data so that every -// entry is rounded to an integer and the sum is the same as -// that of the input. Works by rounding an entry in the list -// and passing the rounding error forward to the next entry. -// This will generally distribute the error in a uniform manner. -function _sum_preserving_round(data, index=0) = - index == len(data)-1 ? list_set(data, len(data)-1, round(data[len(data)-1])) : - let( - newval = round(data[index]), - error = newval - data[index] - ) _sum_preserving_round( - list_set(data, [index,index+1], [newval, data[index+1]-error]), - index+1 - ); - - -// Function: subdivide_path() -// Usage: -// newpath = subdivide_path(path, [N|refine], method); -// Description: -// Takes a path as input (closed or open) and subdivides the path to produce a more -// finely sampled path. The new points can be distributed proportional to length -// (`method="length"`) or they can be divided up evenly among all the path segments -// (`method="segment"`). If the extra points don't fit evenly on the path then the -// algorithm attempts to distribute them uniformly. The `exact` option requires that -// the final length is exactly as requested. If you set it to `false` then the -// algorithm will favor uniformity and the output path may have a different number of -// points due to rounding error. -// . -// With the `"segment"` method you can also specify a vector of lengths. This vector, -// `N` specfies the desired point count on each segment: with vector input, `subdivide_path` -// attempts to place `N[i]-1` points on segment `i`. The reason for the -1 is to avoid -// double counting the endpoints, which are shared by pairs of segments, so that for -// a closed polygon the total number of points will be sum(N). Note that with an open -// path there is an extra point at the end, so the number of points will be sum(N)+1. -// Arguments: -// path = path to subdivide -// N = scalar total number of points desired or with `method="segment"` can be a vector requesting `N[i]-1` points on segment i. -// refine = number of points to add each segment. -// closed = set to false if the path is open. Default: True -// exact = if true return exactly the requested number of points, possibly sacrificing uniformity. If false, return uniform point sample that may not match the number of points requested. Default: True -// method = One of `"length"` or `"segment"`. If `"length"`, adds vertices evenly along the total path length. If `"segment"`, adds points evenly among the segments. Default: `"length"` -// Example(2D): -// mypath = subdivide_path(square([2,2],center=true), 12); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): -// mypath = subdivide_path(square([8,2],center=true), 12); -// move_copies(mypath)circle(r=.2,$fn=32); -// Example(2D): -// mypath = subdivide_path(square([8,2],center=true), 12, method="segment"); -// move_copies(mypath)circle(r=.2,$fn=32); -// Example(2D): -// mypath = subdivide_path(square([2,2],center=true), 17, closed=false); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): Specifying different numbers of points on each segment -// mypath = subdivide_path(hexagon(side=2), [2,3,4,5,6,7], method="segment"); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): Requested point total is 14 but 15 points output due to extra end point -// mypath = subdivide_path(pentagon(side=2), [3,4,3,4], method="segment", closed=false); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): Since 17 is not divisible by 5, a completely uniform distribution is not possible. -// mypath = subdivide_path(pentagon(side=2), 17); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): With `exact=false` a uniform distribution, but only 15 points -// mypath = subdivide_path(pentagon(side=2), 17, exact=false); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(2D): With `exact=false` you can also get extra points, here 20 instead of requested 18 -// mypath = subdivide_path(pentagon(side=2), 18, exact=false); -// move_copies(mypath)circle(r=.1,$fn=32); -// Example(FlatSpin,VPD=15,VPT=[0,0,1.5]): Three-dimensional paths also work -// mypath = subdivide_path([[0,0,0],[2,0,1],[2,3,2]], 12); -// move_copies(mypath)sphere(r=.1,$fn=32); -function subdivide_path(path, N, refine, closed=true, exact=true, method="length") = - assert(is_path(path)) - assert(method=="length" || method=="segment") - assert(num_defined([N,refine]),"Must give exactly one of N and refine") - let( - N = !is_undef(N)? N : - !is_undef(refine)? len(path) * refine : - undef - ) - assert((is_num(N) && N>0) || is_vector(N),"Parameter N to subdivide_path must be postive number or vector") - let( - count = len(path) - (closed?0:1), - add_guess = method=="segment"? ( - is_list(N)? ( - assert(len(N)==count,"Vector parameter N to subdivide_path has the wrong length") - add_scalar(N,-1) - ) : repeat((N-len(path)) / count, count) - ) : // method=="length" - assert(is_num(N),"Parameter N to subdivide path must be a number when method=\"length\"") - let( - path_lens = concat( - [ for (i = [0:1:len(path)-2]) norm(path[i+1]-path[i]) ], - closed? [norm(path[len(path)-1]-path[0])] : [] - ), - add_density = (N - len(path)) / sum(path_lens) - ) - path_lens * add_density, - add = exact? _sum_preserving_round(add_guess) : - [for (val=add_guess) round(val)] - ) concat( - [ - for (i=[0:1:count]) each [ - for(j=[0:1:add[i]]) - lerp(path[i],select(path,i+1), j/(add[i]+1)) - ] - ], - closed? [] : [last(path)] - ); - - -// Function: path_length_fractions() -// Usage: -// fracs = path_length_fractions(path, [closed]); -// Description: -// Returns the distance fraction of each point in the path along the path, so the first -// point is zero and the final point is 1. If the path is closed the length of the output -// will have one extra point because of the final connecting segment that connects the last -// point of the path to the first point. -function path_length_fractions(path, closed=false) = - assert(is_path(path)) - assert(is_bool(closed)) - let( - lengths = [ - 0, - for (i=[0:1:len(path)-(closed?1:2)]) - norm(select(path,i+1)-path[i]) - ], - partial_len = cumsum(lengths), - total_len = last(partial_len) - ) partial_len / total_len; - - -// Function: resample_path() -// Usage: -// newpath = resample_path(path, N|spacing, [closed]); -// Description: -// Compute a uniform resampling of the input path. If you specify `N` then the output path will have N -// points spaced uniformly (by linear interpolation along the input path segments). The only points of the -// input path that are guaranteed to appear in the output path are the starting and ending points. -// If you specify `spacing` then the length you give will be rounded to the nearest spacing that gives -// a uniform sampling of the path and the resulting uniformly sampled path is returned. -// Note that because this function operates on a discrete input path the quality of the output depends on -// the sampling of the input. If you want very accurate output, use a lot of points for the input. -// Arguments: -// path = path to resample -// N = Number of points in output -// spacing = Approximate spacing desired -// closed = set to true if path is closed. Default: false -function resample_path(path, N, spacing, closed=false) = - assert(is_path(path)) - assert(num_defined([N,spacing])==1,"Must define exactly one of N and spacing") - assert(is_bool(closed)) - let( - length = path_length(path,closed), - // In the open path case decrease N by 1 so that we don't try to get - // path_cut to return the endpoint (which might fail due to rounding) - // Add last point later - N = is_def(N) ? N-(closed?0:1) : round(length/spacing), - distlist = lerpn(0,length,N,false), - cuts = _path_cut_points(path, distlist, closed=closed) - ) - [ each subindex(cuts,0), - if (!closed) last(path) // Then add last point here - ]; // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/phillips_drive.scad b/phillips_drive.scad deleted file mode 100644 index 9ab829a..0000000 --- a/phillips_drive.scad +++ /dev/null @@ -1,77 +0,0 @@ -////////////////////////////////////////////////////////////////////// -// LibFile: phillips_drive.scad -// Phillips driver bits -// Includes: -// include -// include -////////////////////////////////////////////////////////////////////// - - -// Section: Modules - - -// Module: phillips_drive() -// Description: Creates a model of a phillips driver bit of a given named size. -// Arguments: -// size = The size of the bit as a string. "#0", "#1", "#2", "#3", or "#4" -// shaft = The diameter of the drive bit's shaft. -// l = The length of the drive bit. -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` -// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` -// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` -// Example: -// xdistribute(10) { -// phillips_drive(size="#1", shaft=4, l=20); -// phillips_drive(size="#2", shaft=6, l=20); -// phillips_drive(size="#3", shaft=6, l=20); -// } -module phillips_drive(size="#2", shaft=6, l=20, $fn=36, anchor=BOTTOM, spin=0, orient=UP) { - assert(is_string(size)); - assert(in_list(size,["#0","#1","#2","#3","#4"])); - - num = ord(size[1]) - ord("0"); - b = [0.61, 0.97, 1.47, 2.41, 3.48][num]; - e = [0.31, 0.43, 0.81, 2.00, 2.41][num]; - g = [0.81, 1.27, 2.29, 3.81, 5.08][num]; - //f = [0.33, 0.53, 0.70, 0.82, 1.23][num]; - //r = [0.30, 0.50, 0.60, 0.80, 1.00][num]; - alpha = [ 136, 138, 140, 146, 153][num]; - beta = [7.00, 7.00, 5.75, 5.75, 7.00][num]; - gamma = 92.0; - ang1 = 28.0; - ang2 = 26.5; - h1 = adj_ang_to_opp(g/2, ang1); - h2 = adj_ang_to_opp((shaft-g)/2, 90-ang2); - h3 = adj_ang_to_opp(b/2, ang1); - p0 = [0,0]; - p1 = [e/2, adj_ang_to_opp(e/2, 90-alpha/2)]; - p2 = p1 + [(shaft-e)/2, adj_ang_to_hyp((shaft-e)/2, 90-gamma/2)]; - attachable(anchor,spin,orient, d=shaft, l=l) { - down(l/2) { - difference() { - union() { - cyl(d1=0, d2=g, h=h1, anchor=BOT); - up(h1) { - cyl(d1=g, d2=shaft, h=h2, anchor=BOT); - up(h2) cyl(d=shaft, h=l-h1-h2, anchor=BOT); - } - } - zrot(45) - zrot_copies(n=4, r=b/2/cos(90-alpha/2), sa=90) { - up(h3) { - xrot(-beta) { - linear_extrude(height=(h1+h2)*20, convexity=4, center=true) { - path = [p0, p1, p2, [-p2.x,p2.y], [-p1.x,p1.y]]; - polygon(path); - } - } - } - } - } - } - children(); - } -} - - -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/regions.scad b/regions.scad index c7d771d..07e1638 100644 --- a/regions.scad +++ b/regions.scad @@ -29,31 +29,17 @@ function is_region(x) = is_list(x) && is_path(x.x); function close_region(region, eps=EPSILON) = [for (path=region) close_path(path, eps=eps)]; -// Module: region() +// Function: cleanup_region() // Usage: -// region(r); +// cleanup_region(region); // Description: -// Creates 2D polygons for the given region. The region given is a list of closed 2D paths. -// Each path will be effectively exclusive-ORed from all other paths in the region, so if a -// path is inside another path, it will be effectively subtracted from it. -// Example(2D): -// region([circle(d=50), square(25,center=true)]); -// Example(2D): -// rgn = concat( -// [for (d=[50:-10:10]) circle(d=d-5)], -// [square([60,10], center=true)] -// ); -// region(rgn); -module region(r) -{ - points = flatten(r); - paths = [ - for (i=[0:1:len(r)-1]) let( - start = default(sum([for (j=[0:1:i-1]) len(r[j])]),0) - ) [for (k=[0:1:len(r[i])-1]) start+k] - ]; - polygon(points=points, paths=paths); -} +// For all paths in the given region, if the last point coincides with the first point, removes the last point. +// Arguments: +// region = The region to clean up. Given as a list of polygon paths. +// eps = Acceptable variance. Default: `EPSILON` (1e-9) +function cleanup_region(region, eps=EPSILON) = + [for (path=region) cleanup_path(path, eps=eps)]; + // Function: check_and_fix_path() @@ -91,16 +77,34 @@ function check_and_fix_path(path, valid_dim=undef, closed=false, name="path") = closed && approx(path[0], last(path))? list_head(path) : path; -// Function: cleanup_region() + + +// Module: region() // Usage: -// cleanup_region(region); +// region(r); // Description: -// For all paths in the given region, if the last point coincides with the first point, removes the last point. -// Arguments: -// region = The region to clean up. Given as a list of polygon paths. -// eps = Acceptable variance. Default: `EPSILON` (1e-9) -function cleanup_region(region, eps=EPSILON) = - [for (path=region) cleanup_path(path, eps=eps)]; +// Creates 2D polygons for the given region. The region given is a list of closed 2D paths. +// Each path will be effectively exclusive-ORed from all other paths in the region, so if a +// path is inside another path, it will be effectively subtracted from it. +// Example(2D): +// region([circle(d=50), square(25,center=true)]); +// Example(2D): +// rgn = concat( +// [for (d=[50:-10:10]) circle(d=d-5)], +// [square([60,10], center=true)] +// ); +// region(rgn); +module region(r) +{ + points = flatten(r); + paths = [ + for (i=[0:1:len(r)-1]) let( + start = default(sum([for (j=[0:1:i-1]) len(r[j])]),0) + ) [for (k=[0:1:len(r[i])-1]) start+k] + ]; + polygon(points=points, paths=paths); +} + // Function: point_in_region() @@ -121,11 +125,12 @@ function point_in_region(point, region, eps=EPSILON, _i=0, _cnt=0) = ) pip==0? 0 : point_in_region(point, region, eps=eps, _i=_i+1, _cnt = _cnt + (pip>0? 1 : 0)); + // Function: polygons_equal() // Usage: // b = polygons_equal(poly1, poly2, [eps]) // Description: -// Returns true if the components of region1 and region2 are the same polygons +// Returns true if poly1 and poly2 are the same polongs // within given epsilon tolerance. // Arguments: // poly1 = first polygon @@ -151,34 +156,34 @@ function __polygons_equal(poly1, poly2, eps, st) = max([for(d=poly1-select(poly2,st,st-1)) d*d])= len(polys)? false : polygons_equal(poly, polys[i])? true : - __poly_in_polygons(poly, polys, i+1); + __is_polygon_in_list(poly, polys, i+1); // Function: regions_equal() // Usage: // b = regions_equal(region1, region2, [eps]) // Description: -// Returns true if the components of region1 and region2 are the same polygons +// Returns true if the components of region1 and region2 are the same polygons (in any order) // within given epsilon tolerance. // Arguments: -// poly1 = first polygon -// poly2 = second polygon +// region1 = first region +// region2 = second region // eps = tolerance for comparison function regions_equal(region1, region2) = assert(is_region(region1) && is_region(region2)) @@ -187,21 +192,21 @@ function regions_equal(region1, region2) = function __regions_equal(region1, region2, i) = i >= len(region1)? true : - !poly_in_polygons(region1[i], region2)? false : + !is_polygon_in_list(region1[i], region2)? false : __regions_equal(region1, region2, i+1); -// Function: region_path_crossings() -// Usage: -// region_path_crossings(path, region); -// Description: -// Returns a sorted list of [SEGMENT, U] that describe where a given path is crossed by a second path. -// Arguments: -// path = The path to find crossings on. -// region = Region to test for crossings of. -// closed = If true, treat path as a closed polygon. Default: true -// eps = Acceptable variance. Default: `EPSILON` (1e-9) -function region_path_crossings(path, region, closed=true, eps=EPSILON) = sort([ +/// Internal Function: _region_path_crossings() +/// Usage: +/// _region_path_crossings(path, region); +/// Description: +/// Returns a sorted list of [SEGMENT, U] that describe where a given path is crossed by a second path. +/// Arguments: +/// path = The path to find crossings on. +/// region = Region to test for crossings of. +/// closed = If true, treat path as a closed polygon. Default: true +/// eps = Acceptable variance. Default: `EPSILON` (1e-9) +function _region_path_crossings(path, region, closed=true, eps=EPSILON) = sort([ let( segs = pair(closed? close_path(path) : cleanup_path(path)) ) for ( @@ -240,7 +245,7 @@ function split_path_at_region_crossings(path, region, closed=true, eps=EPSILON) let( path = deduplicate(path, eps=eps), region = [for (path=region) deduplicate(path, eps=eps)], - xings = region_path_crossings(path, region, closed=closed, eps=eps), + xings = _region_path_crossings(path, region, closed=closed, eps=eps), crossings = deduplicate( concat([[0,0]], xings, [[len(path)-1,1]]), eps=eps @@ -489,7 +494,7 @@ function _shift_segment(segment, d) = // Extend to segments to their intersection point. First check if the segments already have a point in common, // which can happen if two colinear segments are input to the path variant of `offset()` function _segment_extension(s1,s2) = - norm(s1[1]-s2[0])<1e-6 ? s1[1] : line_intersection(s1,s2); + norm(s1[1]-s2[0])<1e-6 ? s1[1] : line_intersection(s1,s2,LINE,LINE); function _makefaces(direction, startind, good, pointcount, closed) = @@ -745,7 +750,11 @@ function offset( quality = max(0,round(quality)), flip_dir = closed && !is_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)], +// shiftsegs = [for(i=[0:len(path)-1]) _shift_segment(select(path,i,i+1), d)], + shiftsegs = [for(i=[0:len(path)-2]) _shift_segment([path[i],path[i+1]], d), + if (closed) _shift_segment([last(path),path[0]],d) + else [path[0],path[1]] // dummy segment, not used + ], // 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) : repeat(true,len(shiftsegs)), goodsegs = bselect(shiftsegs, good), @@ -759,7 +768,8 @@ function offset( // Note if !closed the last corner doesn't matter, so exclude it parallelcheck = (len(sharpcorners)==2 && !closed) || - all_defined(closed? sharpcorners : list_tail(sharpcorners)) + all_defined(closed? sharpcorners : select(sharpcorners, 1,-2)), + f=echo(sharpcorners=sharpcorners) ) assert(parallelcheck, "Path contains sequential parallel segments (either 180 deg turn or 0 deg turn") let( @@ -771,7 +781,7 @@ function offset( : [for(i=[0:len(goodsegs)-1]) let(prevseg=select(goodsegs,i-1)) - i==0 && !closed ? false // In open case first entry is bogus + (i==0 || i==len(goodsegs)-1) && !closed ? false // In open case first entry is bogus : (goodsegs[i][1]-goodsegs[i][0]) * (goodsegs[i][0]-sharpcorners[i]) > 0 && (prevseg[1]-prevseg[0]) * (sharpcorners[i]-prevseg[1]) > 0 diff --git a/rounding.scad b/rounding.scad index 154b218..da18760 100644 --- a/rounding.scad +++ b/rounding.scad @@ -1931,8 +1931,8 @@ function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_b top_patch[i][4][4] ] ], - top_intersections = path_self_intersections(faces[0]), - bot_intersections = path_self_intersections(faces[1]), + top_simple = is_path_simple(faces[0],closed=true), + bot_simple = is_path_simple(faces[1],closed=true), // verify vertical edges verify_vert = [for(i=[0:N-1],j=[0:4]) @@ -1949,9 +1949,9 @@ function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_b ) if (!is_collinear(hline_top) || !is_collinear(hline_bot)) [i,j]] ) - assert(debug || top_intersections==[], + assert(debug || top_simple, "Roundovers interfere with each other on top face: either input is self intersecting or top joint length is too large") - assert(debug || bot_intersections==[], + assert(debug || bot_simple, "Roundovers interfere with each other on bottom face: either input is self intersecting or top joint length is too large") assert(debug || (verify_vert==[] && verify_horiz==[]), "Curvature continuity failed") let( diff --git a/screw_drive.scad b/screw_drive.scad new file mode 100644 index 0000000..d66eb38 --- /dev/null +++ b/screw_drive.scad @@ -0,0 +1,312 @@ +////////////////////////////////////////////////////////////////////// +// LibFile: screw_drive.scad +// Recess masks for screw heads +// Includes: +// include +// include +////////////////////////////////////////////////////////////////////// + + +// Section: Phillips Drive + +// Module: phillips_mask() +// Description: +// Creates a mask for creating a Phillips drive recess given the Phillips size. Each mask can +// be lowered to different depths to create different sizes of recess. +// Arguments: +// size = The size of the bit as a number or string. "#0", "#1", "#2", "#3", or "#4" +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// Example: +// xdistribute(10) { +// phillips_mask(size="#1"); +// phillips_mask(size="#2"); +// phillips_mask(size=3); +// phillips_mask(size=4); +// } + +// Specs for phillips recess here: +// https://www.fasteners.eu/tech-info/ISO/4757/ + +_phillips_shaft = [3,4.5,6,8,10]; +_ph_bot_angle = 28.0; +_ph_side_angle = 26.5; + +module phillips_mask(size="#2", $fn=36, anchor=BOTTOM, spin=0, orient=UP) { + assert(in_list(size,["#0","#1","#2","#3","#4",0,1,2,3,4])); + num = is_num(size) ? size : ord(size[1]) - ord("0"); + shaft = _phillips_shaft[num]; + b = [0.61, 0.97, 1.47, 2.41, 3.48][num]; + e = [0.31, 0.435, 0.815, 2.005, 2.415][num]; + g = [0.81, 1.27, 2.29, 3.81, 5.08][num]; + //f = [0.33, 0.53, 0.70, 0.82, 1.23][num]; + //r = [0.30, 0.50, 0.60, 0.80, 1.00][num]; + alpha = [ 136, 138, 140, 146, 153][num]; + beta = [7.00, 7.00, 5.75, 5.75, 7.00][num]; + gamma = 92.0; + h1 = adj_ang_to_opp(g/2, _ph_bot_angle); // height of the small conical tip + h2 = adj_ang_to_opp((shaft-g)/2, 90-_ph_side_angle); // height of larger cone + l = h1+h2; + h3 = adj_ang_to_opp(b/2, _ph_bot_angle); // height where cutout starts + p0 = [0,0]; + p1 = [adj_ang_to_opp(e/2, 90-alpha/2), -e/2]; + p2 = p1 + [adj_ang_to_opp((shaft-e)/2, 90-gamma/2),-(shaft-e)/2]; + attachable(anchor,spin,orient, d=shaft, l=l) { + down(l/2) { + difference() { + rotate_extrude() + polygon([[0,0],[g/2,h1],[shaft/2,l],[0,l]]); + zrot(45) + zrot_copies(n=4, r=b/2) { + up(h3) { + yrot(beta) { + down(1) + linear_extrude(height=l+2, convexity=4, center=false) { + path = [p0, p1, p2, [p2.x,-p2.y], [p1.x,-p1.y]]; + polygon(path); + } + } + } + } + } + } + children(); + } +} + + + +// Function: phillips_depth() +// Usage: +// depth = phillips_depth(size, d); +// Description: +// Returns the depth of the Phillips recess required to produce the specified diameter, or +// undef if not possible. +// Arguments: +// size = size as a number or text string like "#2" +// d = desired diameter +function phillips_depth(size, d) = + assert(in_list(size,["#0","#1","#2","#3","#4",0,1,2,3,4])) + let( + num = is_num(size) ? size : ord(size[1]) - ord("0"), + shaft = [3,4.5,6,8,10][num], + g = [0.81, 1.27, 2.29, 3.81, 5.08][num], + _ph_bot_angle = 28.0, + _ph_side_angle = 26.5, + h1 = adj_ang_to_opp(g/2, _ph_bot_angle), // height of the small conical tip + h2 = adj_ang_to_opp((shaft-g)/2, 90-_ph_side_angle) // height of larger cone + ) + d>=shaft || d= h1+h2 ? undef : + 2 * tan(_ph_side_angle)*(depth-h1) + g; + + + +// Section: Torx Drive + + +// Function: torx_outer_diam() +// Description: Get the typical outer diameter of Torx profile. +// Arguments: +// size = Torx size. +function torx_outer_diam(size) = lookup(size, [ + [ 6, 1.75], + [ 8, 2.40], + [ 10, 2.80], + [ 15, 3.35], + [ 20, 3.95], + [ 25, 4.50], + [ 30, 5.60], + [ 40, 6.75], + [ 45, 7.93], + [ 50, 8.95], + [ 55, 11.35], + [ 60, 13.45], + [ 70, 15.70], + [ 80, 17.75], + [ 90, 20.20], + [100, 22.40] +]); + + +// Function: torx_inner_diam() +// Description: Get typical inner diameter of Torx profile. +// Arguments: +// size = Torx size. +function torx_inner_diam(size) = lookup(size, [ + [ 6, 1.27], + [ 8, 1.75], + [ 10, 2.05], + [ 15, 2.40], + [ 20, 2.85], + [ 25, 3.25], + [ 30, 4.05], + [ 40, 4.85], + [ 45, 5.64], + [ 50, 6.45], + [ 55, 8.05], + [ 60, 9.60], + [ 70, 11.20], + [ 80, 12.80], + [ 90, 14.40], + [100, 16.00] +]); + + +// Function: torx_depth() +// Description: Gets typical drive hole depth. +// Arguments: +// size = Torx size. +function torx_depth(size) = lookup(size, [ + [ 6, 1.82], + [ 8, 3.05], + [ 10, 3.56], + [ 15, 3.81], + [ 20, 4.07], + [ 25, 4.45], + [ 30, 4.95], + [ 40, 5.59], + [ 45, 6.22], + [ 50, 6.48], + [ 55, 6.73], + [ 60, 8.17], + [ 70, 8.96], + [ 80, 9.90], + [ 90, 10.56], + [100, 11.35] +]); + + +// Function: torx_tip_radius() +// Description: Gets minor rounding radius of Torx profile. +// Arguments: +// size = Torx size. +function torx_tip_radius(size) = lookup(size, [ + [ 6, 0.132], + [ 8, 0.190], + [ 10, 0.229], + [ 15, 0.267], + [ 20, 0.305], + [ 25, 0.375], + [ 30, 0.451], + [ 40, 0.546], + [ 45, 0.574], + [ 50, 0.775], + [ 55, 0.867], + [ 60, 1.067], + [ 70, 1.194], + [ 80, 1.526], + [ 90, 1.530], + [100, 1.720] +]); + + +// Function: torx_rounding_radius() +// Description: Gets major rounding radius of Torx profile. +// Arguments: +// size = Torx size. +function torx_rounding_radius(size) = lookup(size, [ + [ 6, 0.383], + [ 8, 0.510], + [ 10, 0.598], + [ 15, 0.716], + [ 20, 0.859], + [ 25, 0.920], + [ 30, 1.194], + [ 40, 1.428], + [ 45, 1.796], + [ 50, 1.816], + [ 55, 2.667], + [ 60, 2.883], + [ 70, 3.477], + [ 80, 3.627], + [ 90, 4.468], + [100, 4.925] +]); + + + +// Module: torx_mask2d() +// Description: Creates a torx bit 2D profile. +// Arguments: +// size = Torx size. +// Example(2D): +// torx_mask2d(size=30, $fa=1, $fs=1); +module torx_mask2d(size) { + od = torx_outer_diam(size); + id = torx_inner_diam(size); + tip = torx_tip_radius(size); + rounding = torx_rounding_radius(size); + base = od - 2*tip; + $fn = quantup(segs(od/2),12); + difference() { + union() { + circle(d=base); + zrot_copies(n=2) { + hull() { + zrot_copies(n=3) { + translate([base/2,0,0]) { + circle(r=tip, $fn=$fn/2); + } + } + } + } + } + zrot_copies(n=6) { + zrot(180/6) { + translate([id/2+rounding,0,0]) { + circle(r=rounding); + } + } + } + } +} + + + +// Module: torx_mask() +// Description: Creates a torx bit tip. +// Arguments: +// size = Torx size. +// l = Length of bit. +// center = If true, centers bit vertically. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// Examples: +// torx_mask(size=30, l=10, $fa=1, $fs=1); +module torx_mask(size, l=5, center, anchor, spin=0, orient=UP) { + anchor = get_anchor(anchor, center, BOT, BOT); + od = torx_outer_diam(size); + attachable(anchor,spin,orient, d=od, l=l) { + linear_extrude(height=l, convexity=4, center=true) { + torx_mask2d(size); + } + children(); + } +} + + +// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/screws.scad b/screws.scad index 9a0e9a7..e0d4e43 100644 --- a/screws.scad +++ b/screws.scad @@ -8,8 +8,8 @@ include include -include -include +include + // Section: Generic Screw Creation @@ -837,18 +837,18 @@ module screw_head(screw_info,details=false) { // anchor = anchor relative to the shaft of the screw // anchor_head = anchor relative to the screw head // Example(Med): Selected UTS (English) screws - $fn=32; - xdistribute(spacing=8){ - screw("#6", length=12); - screw("#6-32", head="button", drive="torx",length=12); - screw("#6-32,3/4", head="hex"); - screw("#6", thread="fine", head="fillister",length=12, drive="phillips"); - screw("#6", head="flat small",length=12,drive="slot"); - screw("#6-32", head="flat large", length=12, drive="torx"); - screw("#6-32", head="flat undercut",length=12); - screw("#6-24", head="socket",length=12); // Non-standard threading - screw("#6-32", drive="hex", drive_size=1.5, length=12); - } +// $fn=32; +// xdistribute(spacing=8){ +// screw("#6", length=12); +// screw("#6-32", head="button", drive="torx",length=12); +// screw("#6-32,3/4", head="hex"); +// screw("#6", thread="fine", head="fillister",length=12, drive="phillips"); +// screw("#6", head="flat small",length=12,drive="slot"); +// screw("#6-32", head="flat large", length=12, drive="torx"); +// screw("#6-32", head="flat undercut",length=12); +// screw("#6-24", head="socket",length=12); // Non-standard threading +// screw("#6-32", drive="hex", drive_size=1.5, length=12); +// } // Example(Med): A few examples of ISO (metric) screws // $fn=32; // xdistribute(spacing=8){ @@ -1037,8 +1037,8 @@ module _driver(spec) echo(drive_size=drive_size); up(head_top-drive_depth){ // recess should be positioned with its bottom center at (0,0) and the correct recess depth given above - if (drive=="phillips") phillips_drive(size=str("#",drive_size), shaft=diameter,anchor=BOTTOM); - if (drive=="torx") torx_drive(size=drive_size, l=drive_depth+1, center=false); + if (drive=="phillips") phillips_mask(drive_size,anchor=BOTTOM); + if (drive=="torx") torx_mask(size=drive_size, l=drive_depth+1, center=false); if (drive=="hex") linear_extrude(height=drive_depth+1) hexagon(id=drive_size); if (drive=="slot") cuboid([2*struct_val(spec,"head_size"), drive_width, drive_depth+1],anchor=BOTTOM); } @@ -1415,9 +1415,21 @@ http://files.engineering.com/getfile.aspx?folder=76fb0d5e-1fff-4c49-87a5-0597947 // // Torx drive depth for UTS and ISO (at least missing for "flat small", which means you can't select torx for this head type) // Handle generic phillips (e.g. ph2) or remove it? + +// https://www.fasteners.eu/tech-info/ISO/7721-2/ // // How do you insert a threaded hole into a model? // Default nut thickness // +// JIS +//https://www.garagejournal.com/forum/media/jis-b-4633-vs-iso-8764-1-din-5260-ph.84492/ + +//square: +//https://www.aspenfasteners.com/content/pdf/square_drive_specification.pdf +//http://www.globalfastener.com/standards/index.php?narr58=149 +//https://patents.google.com/patent/US1003657 + +// thread standards: +// https://www.gewinde-normen.de/en/index.html // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/shapes2d.scad b/shapes2d.scad index 1849c83..d99ed29 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -810,9 +810,45 @@ module star(n, r, ir, d, or, od, id, step, realign=false, align_tip, align_pit, +/// Internal Function: _path_add_jitter() +/// Topics: Paths +/// See Also: jittered_poly(), subdivide_long_segments() +/// Usage: +/// jpath = _path_add_jitter(path, [dist], [closed=]); +/// Description: +/// Adds tiny jitter offsets to collinear points in the given path so that they +/// are no longer collinear. This is useful for preserving subdivision on long +/// straight segments, when making geometry with `polygon()`, for use with +/// `linear_exrtrude()` with a `twist()`. +/// Arguments: +/// path = The path to add jitter to. +/// dist = The amount to jitter points by. Default: 1/512 (0.00195) +/// --- +/// closed = If true, treat path like a closed polygon. Default: true +/// Example(3D): +/// d = 100; h = 75; quadsize = 5; +/// path = pentagon(d=d); +/// spath = subdivide_long_segments(path, quadsize, closed=true); +/// jpath = _path_add_jitter(spath, closed=true); +/// linear_extrude(height=h, twist=72, slices=h/quadsize) +/// polygon(jpath); +function _path_add_jitter(path, dist=1/512, closed=true) = + assert(is_path(path)) + assert(is_finite(dist)) + assert(is_bool(closed)) + [ + path[0], + for (i=idx(path,s=1,e=closed?-1:-2)) let( + n = line_normal([path[i-1],path[i]]) + ) path[i] + n * (is_collinear(select(path,i-1,i+1))? (dist * ((i%2)*2-1)) : 0), + if (!closed) last(path) + ]; + + + // Module: jittered_poly() // Topics: Extrusions -// See Also: path_add_jitter(), subdivide_long_segments() +// See Also: _path_add_jitter(), subdivide_long_segments() // Usage: // jittered_poly(path, [dist]); // Description: @@ -829,7 +865,7 @@ module star(n, r, ir, d, or, od, id, step, realign=false, align_tip, align_pit, // linear_extrude(height=h, twist=72, slices=h/quadsize) // jittered_poly(spath); module jittered_poly(path, dist=1/512) { - polygon(path_add_jitter(path, dist, closed=true)); + polygon(_path_add_jitter(path, dist, closed=true)); } diff --git a/shapes3d.scad b/shapes3d.scad index 1a135f4..ab01138 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -1,6 +1,10 @@ ////////////////////////////////////////////////////////////////////// // LibFile: shapes3d.scad -// Common useful shapes and structured objects. +// Some standard modules for making 3d shapes with attachment support, and function forms +// that produce a VNF. Also included are shortcuts cylinders in each orientation and extended versions of +// the standard modules that provide roundovers and chamfers. The sphereoid() module provides +// several different ways to make a sphere, and the text modules let you write text on a path +// so you can place it on a curved object. // Includes: // include ////////////////////////////////////////////////////////////////////// @@ -1396,6 +1400,61 @@ module tube( +// Module: pie_slice() +// +// Description: +// Creates a pie slice shape. +// +// Usage: Typical +// pie_slice(l|h, r, ang, [center]); +// pie_slice(l|h, d=, ang=, ...); +// pie_slice(l|h, r1=|d1=, r2=|d2=, ang=, ...); +// Usage: Attaching Children +// pie_slice(l|h, r, ang, ...) [attachments]; +// +// Arguments: +// h / l = height of pie slice. +// r = radius of pie slice. +// ang = pie slice angle in degrees. +// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=UP`. +// --- +// r1 = bottom radius of pie slice. +// r2 = top radius of pie slice. +// d = diameter of pie slice. +// d1 = bottom diameter of pie slice. +// d2 = top diameter of pie slice. +// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` +// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` +// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` +// +// Example: Cylindrical Pie Slice +// pie_slice(ang=45, l=20, r=30); +// Example: Conical Pie Slice +// pie_slice(ang=60, l=20, d1=50, d2=70); +module pie_slice( + h, r, ang=30, center, + r1, r2, d, d1, d2, l, + anchor, spin=0, orient=UP +) { + l = first_defined([l, h, 1]); + r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=10); + r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=10); + maxd = max(r1,r2)+0.1; + anchor = get_anchor(anchor, center, BOT, BOT); + attachable(anchor,spin,orient, r1=r1, r2=r2, l=l) { + difference() { + cyl(r1=r1, r2=r2, h=l); + if (ang<180) rotate(ang) back(maxd/2) cube([2*maxd, maxd, l+0.1], center=true); + difference() { + fwd(maxd/2) cube([2*maxd, maxd, l+0.2], center=true); + if (ang>180) rotate(ang-180) back(maxd/2) cube([2*maxd, maxd, l+0.1], center=true); + } + } + children(); + } +} + + // Section: Other Round Objects @@ -2204,59 +2263,6 @@ module nil() union(){} module noop(spin=0, orient=UP) attachable(CENTER,spin,orient, d=0.01) {nil(); children();} -// Module: pie_slice() -// -// Description: -// Creates a pie slice shape. -// -// Usage: Typical -// pie_slice(l|h, r, ang, [center]); -// pie_slice(l|h, d=, ang=, ...); -// pie_slice(l|h, r1=|d1=, r2=|d2=, ang=, ...); -// Usage: Attaching Children -// pie_slice(l|h, r, ang, ...) [attachments]; -// -// Arguments: -// h / l = height of pie slice. -// r = radius of pie slice. -// ang = pie slice angle in degrees. -// center = If given, overrides `anchor`. A true value sets `anchor=CENTER`, false sets `anchor=UP`. -// --- -// r1 = bottom radius of pie slice. -// r2 = top radius of pie slice. -// d = diameter of pie slice. -// d1 = bottom diameter of pie slice. -// d2 = top diameter of pie slice. -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` -// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` -// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` -// -// Example: Cylindrical Pie Slice -// pie_slice(ang=45, l=20, r=30); -// Example: Conical Pie Slice -// pie_slice(ang=60, l=20, d1=50, d2=70); -module pie_slice( - h, r, ang=30, center, - r1, r2, d, d1, d2, l, - anchor, spin=0, orient=UP -) { - l = first_defined([l, h, 1]); - r1 = get_radius(r1=r1, r=r, d1=d1, d=d, dflt=10); - r2 = get_radius(r1=r2, r=r, d1=d2, d=d, dflt=10); - maxd = max(r1,r2)+0.1; - anchor = get_anchor(anchor, center, BOT, BOT); - attachable(anchor,spin,orient, r1=r1, r2=r2, l=l) { - difference() { - cyl(r1=r1, r2=r2, h=l); - if (ang<180) rotate(ang) back(maxd/2) cube([2*maxd, maxd, l+0.1], center=true); - difference() { - fwd(maxd/2) cube([2*maxd, maxd, l+0.2], center=true); - if (ang>180) rotate(ang-180) back(maxd/2) cube([2*maxd, maxd, l+0.1], center=true); - } - } - children(); - } -} // Module: interior_fillet() diff --git a/skin.scad b/skin.scad index 4640086..be13941 100644 --- a/skin.scad +++ b/skin.scad @@ -832,8 +832,13 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi 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, + normalOK = is_undef(normal) || (method!="natural" && is_vector(normal,3)) + || (method=="manual" && same_shape(normal,path)) ) + assert(normalOK, method=="natural" ? "Cannot specify normal with the \"natural\" method" + : method=="incremental" ? "Normal with \"incremental\" method must be a 3-vector" + : str("Incompatible normal given. Must be a 3-vector or a list of ",len(path)," 3-vectors")) assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") assert(is_undef(normal) || (is_vector(normal) && len(normal)==3) || (is_path(normal) && len(normal)==len(path) && len(normal[0])==3), "Invalid normal specified") @@ -1158,37 +1163,6 @@ function subdivide_and_slice(profiles, slices, numpoints, method="length", close -// Function: subdivide_long_segments() -// Topics: Paths, Path Subdivision -// See Also: subdivide_path(), subdivide_and_slice(), path_add_jitter(), jittered_poly() -// Usage: -// spath = subdivide_long_segments(path, maxlen, [closed=]); -// Description: -// Evenly subdivides long `path` segments until they are all shorter than `maxlen`. -// Arguments: -// path = The path to subdivide. -// maxlen = The maximum allowed path segment length. -// --- -// closed = If true, treat path like a closed polygon. Default: true -// Example: -// path = pentagon(d=100); -// spath = subdivide_long_segments(path, 10, closed=true); -// stroke(path); -// color("lightgreen") move_copies(path) circle(d=5,$fn=12); -// color("blue") move_copies(spath) circle(d=3,$fn=12); -function subdivide_long_segments(path, maxlen, closed=false) = - assert(is_path(path)) - assert(is_finite(maxlen)) - assert(is_bool(closed)) - [ - for (p=pair(path,closed)) let( - steps = ceil(norm(p[1]-p[0])/maxlen) - ) each lerpn(p[0], p[1], steps, false), - if (!closed) last(path) - ]; - - - // Function: slice_profiles() // Topics: Paths, Path Subdivision // Usage: diff --git a/tests/test_torx_drive.scad b/tests/test_screw_drive.scad similarity index 98% rename from tests/test_torx_drive.scad rename to tests/test_screw_drive.scad index 7c4bcf2..398a43e 100644 --- a/tests/test_torx_drive.scad +++ b/tests/test_screw_drive.scad @@ -1,5 +1,5 @@ include <../std.scad> -include <../torx_drive.scad> +include <../screw_drive.scad> module test_torx_outer_diam() { diff --git a/torx_drive.scad b/torx_drive.scad deleted file mode 100644 index abe28c9..0000000 --- a/torx_drive.scad +++ /dev/null @@ -1,199 +0,0 @@ -////////////////////////////////////////////////////////////////////// -// LibFile: torx_drive.scad -// Torx driver bits -// Includes: -// include -// include -////////////////////////////////////////////////////////////////////// - - -// Section: Functions - - -// Function: torx_outer_diam() -// Description: Get the typical outer diameter of Torx profile. -// Arguments: -// size = Torx size. -function torx_outer_diam(size) = lookup(size, [ - [ 6, 1.75], - [ 8, 2.40], - [ 10, 2.80], - [ 15, 3.35], - [ 20, 3.95], - [ 25, 4.50], - [ 30, 5.60], - [ 40, 6.75], - [ 45, 7.93], - [ 50, 8.95], - [ 55, 11.35], - [ 60, 13.45], - [ 70, 15.70], - [ 80, 17.75], - [ 90, 20.20], - [100, 22.40] -]); - - -// Function: torx_inner_diam() -// Description: Get typical inner diameter of Torx profile. -// Arguments: -// size = Torx size. -function torx_inner_diam(size) = lookup(size, [ - [ 6, 1.27], - [ 8, 1.75], - [ 10, 2.05], - [ 15, 2.40], - [ 20, 2.85], - [ 25, 3.25], - [ 30, 4.05], - [ 40, 4.85], - [ 45, 5.64], - [ 50, 6.45], - [ 55, 8.05], - [ 60, 9.60], - [ 70, 11.20], - [ 80, 12.80], - [ 90, 14.40], - [100, 16.00] -]); - - -// Function: torx_depth() -// Description: Gets typical drive hole depth. -// Arguments: -// size = Torx size. -function torx_depth(size) = lookup(size, [ - [ 6, 1.82], - [ 8, 3.05], - [ 10, 3.56], - [ 15, 3.81], - [ 20, 4.07], - [ 25, 4.45], - [ 30, 4.95], - [ 40, 5.59], - [ 45, 6.22], - [ 50, 6.48], - [ 55, 6.73], - [ 60, 8.17], - [ 70, 8.96], - [ 80, 9.90], - [ 90, 10.56], - [100, 11.35] -]); - - -// Function: torx_tip_radius() -// Description: Gets minor rounding radius of Torx profile. -// Arguments: -// size = Torx size. -function torx_tip_radius(size) = lookup(size, [ - [ 6, 0.132], - [ 8, 0.190], - [ 10, 0.229], - [ 15, 0.267], - [ 20, 0.305], - [ 25, 0.375], - [ 30, 0.451], - [ 40, 0.546], - [ 45, 0.574], - [ 50, 0.775], - [ 55, 0.867], - [ 60, 1.067], - [ 70, 1.194], - [ 80, 1.526], - [ 90, 1.530], - [100, 1.720] -]); - - -// Function: torx_rounding_radius() -// Description: Gets major rounding radius of Torx profile. -// Arguments: -// size = Torx size. -function torx_rounding_radius(size) = lookup(size, [ - [ 6, 0.383], - [ 8, 0.510], - [ 10, 0.598], - [ 15, 0.716], - [ 20, 0.859], - [ 25, 0.920], - [ 30, 1.194], - [ 40, 1.428], - [ 45, 1.796], - [ 50, 1.816], - [ 55, 2.667], - [ 60, 2.883], - [ 70, 3.477], - [ 80, 3.627], - [ 90, 4.468], - [100, 4.925] -]); - - -// Section: Modules - - -// Module: torx_drive2d() -// Description: Creates a torx bit 2D profile. -// Arguments: -// size = Torx size. -// Example(2D): -// torx_drive2d(size=30, $fa=1, $fs=1); -module torx_drive2d(size) { - od = torx_outer_diam(size); - id = torx_inner_diam(size); - tip = torx_tip_radius(size); - rounding = torx_rounding_radius(size); - base = od - 2*tip; - $fn = quantup(segs(od/2),12); - difference() { - union() { - circle(d=base); - zrot_copies(n=2) { - hull() { - zrot_copies(n=3) { - translate([base/2,0,0]) { - circle(r=tip, $fn=$fn/2); - } - } - } - } - } - zrot_copies(n=6) { - zrot(180/6) { - translate([id/2+rounding,0,0]) { - circle(r=rounding); - } - } - } - } -} - - - -// Module: torx_drive() -// Description: Creates a torx bit tip. -// Arguments: -// size = Torx size. -// l = Length of bit. -// center = If true, centers bit vertically. -// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#anchor). Default: `CENTER` -// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#spin). Default: `0` -// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#orient). Default: `UP` -// Examples: -// torx_drive(size=30, l=10, $fa=1, $fs=1); -module torx_drive(size, l=5, center, anchor, spin=0, orient=UP) { - anchor = get_anchor(anchor, center, BOT, BOT); - od = torx_outer_diam(size); - attachable(anchor,spin,orient, d=od, l=l) { - linear_extrude(height=l, convexity=4, center=true) { - torx_drive2d(size); - } - children(); - } -} - - - -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap - diff --git a/vnf.scad b/vnf.scad index 0cfe001..3ea55d3 100644 --- a/vnf.scad +++ b/vnf.scad @@ -9,9 +9,8 @@ ////////////////////////////////////////////////////////////////////// -// Creating Polyhedrons with VNF Structures +// Section: Creating Polyhedrons with VNF Structures -// Section: VNF Testing and Access // VNF stands for "Vertices'N'Faces". VNF structures are 2-item lists, `[VERTICES,FACES]` where the // first item is a list of vertex points, and the second is a list of face indices into the vertex // list. Each VNF is self contained, with face indices referring only to its own vertex list. @@ -21,8 +20,6 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. -// Section: Constructing VNFs - // Function: vnf_vertex_array() // Usage: // vnf = vnf_vertex_array(points, [caps], [cap1], [cap2], [style], [reverse], [col_wrap], [row_wrap], [vnf]); @@ -207,12 +204,12 @@ function vnf_vertex_array( // points = List of point lists for each row // row_wrap = If true then add faces connecting the first row and last row. These rows must differ by at most 2 in length. // reverse = Set this to reverse the direction of the faces -// Examples: Each row has one more point than the preceeding one. +// Example: Each row has one more point than the preceeding one. // pts = [for(y=[1:1:10]) [for(x=[0:y-1]) [x,y,y]]]; // vnf = vnf_tri_array(pts); // vnf_wireframe(vnf,d=.1); // color("red")move_copies(flatten(pts)) sphere(r=.15,$fn=9); -// Examples: Each row has one more point than the preceeding one. +// Example: Each row has one more point than the preceeding one. // pts = [for(y=[0:2:10]) [for(x=[-y/2:y/2]) [x,y,y]]]; // vnf = vnf_tri_array(pts); // vnf_wireframe(vnf,d=.1); @@ -277,58 +274,6 @@ function vnf_tri_array(points, row_wrap=false, reverse=false, vnf=EMPTY_VNF) = vnf_merge(cleanup=true, [vnf, [flatten(points), faces]]); -// Function: vnf_add_face() -// Usage: -// vnf_add_face(vnf, pts); -// Description: -// Given a VNF structure and a list of face vertex points, adds the face to the VNF structure. -// Returns the modified VNF structure `[VERTICES, FACES]`. It is up to the caller to make -// sure that the points are in the correct order to make the face normal point outwards. -// Arguments: -// vnf = The VNF structure to add a face to. -// pts = The vertex points for the face. -function vnf_add_face(vnf=EMPTY_VNF, pts) = - assert(is_vnf(vnf)) - assert(is_path(pts)) - let( - res = set_union(vnf[0], pts, get_indices=true), - face = deduplicate(res[0], closed=true) - ) [ - res[1], - concat(vnf[1], len(face)>2? [face] : []) - ]; - - -// Function: vnf_add_faces() -// Usage: -// vnf_add_faces(vnf, faces); -// Description: -// Given a VNF structure and a list of faces, where each face is given as a list of vertex points, -// adds the faces to the VNF structure. Returns the modified VNF structure `[VERTICES, FACES]`. -// It is up to the caller to make sure that the points are in the correct order to make the face -// normals point outwards. -// Arguments: -// vnf = The VNF structure to add a face to. -// faces = The list of faces, where each face is given as a list of vertex points. -function vnf_add_faces(vnf=EMPTY_VNF, faces) = - assert(is_vnf(vnf)) - assert(is_list(faces)) - let( - res = set_union(vnf[0], flatten(faces), get_indices=true), - idxs = res[0], - nverts = res[1], - offs = cumsum([0, for (face=faces) len(face)]), - ifaces = [ - for (i=idx(faces)) [ - for (j=idx(faces[i])) - idxs[offs[i]+j] - ] - ] - ) [ - nverts, - concat(vnf[1],ifaces) - ]; - // Function: vnf_merge() // Usage: @@ -381,6 +326,64 @@ function vnf_merge(vnfs, cleanup=false, eps=EPSILON) = [nverts, nfaces]; + +// Function: vnf_add_face() +// Usage: +// vnf_add_face(vnf, pts); +// Description: +// Given a VNF structure and a list of face vertex points, adds the face to the VNF structure. +// Returns the modified VNF structure `[VERTICES, FACES]`. It is up to the caller to make +// sure that the points are in the correct order to make the face normal point outwards. +// Arguments: +// vnf = The VNF structure to add a face to. +// pts = The vertex points for the face. +function vnf_add_face(vnf=EMPTY_VNF, pts) = + assert(is_vnf(vnf)) + assert(is_path(pts)) + let( + res = set_union(vnf[0], pts, get_indices=true), + face = deduplicate(res[0], closed=true) + ) [ + res[1], + concat(vnf[1], len(face)>2? [face] : []) + ]; + + + +// Function: vnf_add_faces() +// Usage: +// vnf_add_faces(vnf, faces); +// Description: +// Given a VNF structure and a list of faces, where each face is given as a list of vertex points, +// adds the faces to the VNF structure. Returns the modified VNF structure `[VERTICES, FACES]`. +// It is up to the caller to make sure that the points are in the correct order to make the face +// normals point outwards. +// Arguments: +// vnf = The VNF structure to add a face to. +// faces = The list of faces, where each face is given as a list of vertex points. +function vnf_add_faces(vnf=EMPTY_VNF, faces) = + assert(is_vnf(vnf)) + assert(is_list(faces)) + let( + res = set_union(vnf[0], flatten(faces), get_indices=true), + idxs = res[0], + nverts = res[1], + offs = cumsum([0, for (face=faces) len(face)]), + ifaces = [ + for (i=idx(faces)) [ + for (j=idx(faces[i])) + idxs[offs[i]+j] + ] + ] + ) [ + nverts, + concat(vnf[1],ifaces) + ]; + + +// Section: VNF Testing and Access + + // Function: is_vnf() // Usage: // bool = is_vnf(x);