diff --git a/partitions.scad b/partitions.scad index 437c46f..9320dd8 100644 --- a/partitions.scad +++ b/partitions.scad @@ -3,7 +3,7 @@ // Cut objects with a plane, or partition them into interlocking pieces for easy printing of large objects. // Includes: // include -// FileGroup: Advanced Modeling +// FileGroup: Basic Modeling // FileSummary: Cut objects with a plane or partition them into interlocking pieces. // FileFootnotes: STD=Included in std.scad ////////////////////////////////////////////////////////////////////// diff --git a/paths.scad b/paths.scad index f401604..dff9991 100644 --- a/paths.scad +++ b/paths.scad @@ -299,7 +299,7 @@ function _path_self_intersections(path, closed=true, eps=EPSILON) = [isect[0], i, isect[1], j, isect[2]] ]; -// Section: Resampling---changing the number of points in a path +// Section: Resampling—changing the number of points in a path // Input `data` is a list that sums to an integer. @@ -320,31 +320,54 @@ function _sum_preserving_round(data, index=0) = // Function: subdivide_path() +// See Also: subdivide_and_slice(), resample_path(), jittered_poly() // Usage: -// newpath = subdivide_path(path, [N|refine], method, [closed], [exact]); +// newpath = subdivide_path(path, [n|refine|maxlen], [method], [closed], [exact]); // 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. +// finely sampled path. You control the subdivision process by using the `maxlen` arg +// to specify a maximum segment length, or by specifying `n` or `refine`, which request +// a certain point count in the output. // . -// 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 +// You can specify the point count using the `n` option, where +// you give the number of points you want in the output, or you can use +// the `refine` option, where you specify a resampling factor. If `refine=3` then +// the number of points would increase by a factor of three, so a four point square would +// have 12 points after subdivision. With point-count subdivision, the new points can be distributed +// proportional to length (`method="length"`), which is the default, 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 as uniformly as possible, but the result may be uneven. +// The `exact` option, which is true by default, requires that the final point count is +// exactly as requested. For example, if you subdivide a four point square and request `n=13` then one edge will have +// an extra point compared to the others. +// If you set `exact=false` then the +// algorithm will favor uniformity and the output path may have a different number of +// points than you requested, but the sampling will be uniform. In our example of the +// square with `n=13`, you will get only 12 points output, with the same number of points on each edge. +// . +// The points are always distributed uniformly on each segment. The `method="length"` option does +// means that the number of points on a segment is based on its length, but the points are still +// distributed uniformly on each segment, independent of the other segments. +// With the `"segment"` method you can also give `n` as a vector of counts. This +// specifies the desired point count on each segment: with vector valued `n` the `subdivide_path` +// function places `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. +// 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. +// . +// If you use the `maxlen` option then you specify the maximum length segment allowed in the output. +// Each segment is subdivided into the largest number of segments meeting your requirement. As above, +// the sampling is uniform on each segment, independent of the other segments. With the `maxlen` option +// you cannot specify `method` or `exact`. // Arguments: // path = path in any dimension or a 1-region -// 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. +// n = scalar total number of points desired or with `method="segment"` can be a vector requesting `n[i]-1` new points added to segment i. +// --- +// refine = increase total number of points by this factor (Specify only one of n, refine and maxlen) +// maxlen = maximum length segment in the output (Specify only one of n, refine and maxlen) // 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"` +// 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. (Not allowed with maxlen.) Default: true +// method = One of `"length"` or `"segment"`. If `"length"`, adds vertices in proportion to segment length, so short segments get fewer points. If `"segment"`, add points evenly among the segments, so all segments get the same number of points. (Not allowed with maxlen.) Default: `"length"` // Example(2D): // mypath = subdivide_path(square([2,2],center=true), 12); // move_copies(mypath)circle(r=.1,$fn=32); @@ -372,34 +395,58 @@ function _sum_preserving_round(data, index=0) = // 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(2D): Using refine in this example multiplies the point count by 3 by adding 2 points to each edge +// mypath = subdivide_path(pentagon(side=2), refine=3); +// move_copies(mypath)circle(r=.1,$fn=32); +// Example(2D): But note that refine doesn't distribute evenly by segment unless you change the method. with the default method set to `"length"`, the points are distributed with more on the long segments in this example using refine. +// mypath = subdivide_path(square([8,2],center=true), refine=3); +// move_copies(mypath)circle(r=.2,$fn=32); +// Example(2D): In this example with maxlen, every side gets a different number of new points +// path = [[0,0],[0,4],[10,6],[10,0]]; +// spath = subdivide_path(path, maxlen=2, closed=true); +// move_copies(spath) circle(r=.25,$fn=12); // 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") = +function subdivide_path(path, n, refine, maxlen, closed=true, exact, method) = let(path = force_path(path)) assert(is_path(path)) - assert(method=="length" || method=="segment") - assert(num_defined([N,refine]),"Must give exactly one of N and refine") + assert(num_defined([n,refine,maxlen]),"Must give exactly one of n, refine, and maxlen") + is_def(maxlen) ? + assert(is_undef(method), "Cannot give method with maxlen") + assert(is_undef(exact), "Cannot give exact with maxlen") + [ + 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) + ] + : let( - N = !is_undef(N)? N : + exact = default(exact, true), + method = default(method, "length") + ) + assert(method=="length" || method=="segment") + 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") + 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) + 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\"") + assert(is_num(n),"Parameter n to subdivide path must be a number when method=\"length\"") let( path_lens = path_segment_lengths(path,closed), - add_density = (N - len(path)) / sum(path_lens) + add_density = (n - len(path)) / sum(path_lens) ) path_lens * add_density, add = exact? _sum_preserving_round(add_guess) @@ -413,43 +460,12 @@ function subdivide_path(path, N, refine, closed=true, exact=true, method="length -// Function: subdivide_long_segments() -// Topics: Paths, Path Subdivision -// See Also: subdivide_path(), subdivide_and_slice(), 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 = path in any dimension or a 1-region -// maxlen = The maximum allowed path segment length. -// --- -// closed = If true, treat path like a closed polygon. Default: true -// Example(2D): -// path = pentagon(d=100); -// spath = subdivide_long_segments(path, 10, closed=true); -// stroke(path,width=2,closed=true); -// color("red") move_copies(path) circle(d=9,$fn=12); -// color("blue") move_copies(spath) circle(d=5,$fn=12); -function subdivide_long_segments(path, maxlen, closed=true) = - let(path=force_path(path)) - 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]); +// 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 +// 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 @@ -458,7 +474,7 @@ function subdivide_long_segments(path, maxlen, closed=true) = // the sampling of the input. If you want very accurate output, use a lot of points for the input. // Arguments: // path = path in any dimension or a 1-region -// N = Number of points in output +// n = Number of points in output // --- // spacing = Approximate spacing desired // closed = set to true if path is closed. Default: true @@ -484,18 +500,18 @@ function subdivide_long_segments(path, maxlen, closed=true) = // color("red")move_copies(sampled) circle($fn=16); -function resample_path(path, N, spacing, closed=true) = +function resample_path(path, n, spacing, closed=true) = let(path = force_path(path)) assert(is_path(path)) - assert(num_defined([N,spacing])==1,"Must define exactly one of N and spacing") + 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 + // 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), + 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 column(cuts,0), diff --git a/regions.scad b/regions.scad index 1316cf4..8812e66 100644 --- a/regions.scad +++ b/regions.scad @@ -213,7 +213,7 @@ function _polygon_crosses_region(region, poly, eps=EPSILON) = // Description: // We extend the notion of the simple path to regions: a simple region is entirely // non-self-intersecting, meaning that it is formed from a list of simple polygons that -// don't intersect each other at all---not even with corner contact points. +// don't intersect each other at all—not even with corner contact points. // Regions with corner contact are valid but may fail CGAL. Simple regions // should not create problems with CGAL. // Arguments: diff --git a/screws.scad b/screws.scad index d1ab6c3..9de2a13 100644 --- a/screws.scad +++ b/screws.scad @@ -358,7 +358,7 @@ function _screw_info_english(diam, threadcount, head, thread, drive) = [3/8, [0.656, 0.199, undef, 7/32, 45 , 0.122, 0.106]], [7/16, [0.750, 0.220, undef, 1/4, undef, 0.193, undef]], // hex depth interpolated [1/2, [0.875, 0.265, undef, 5/16, 55 , 0.175, 0.158]], - [5/8, [1.000, 0.331, undef, 3/8, 60, , 0.210, 0.192]], + [5/8, [1.000, 0.331, undef, 3/8, 60 , 0.210, 0.192]], [3/4, [1.1, 0.375, undef, 7/16, undef, 0.241]], // hex depth extrapolated ], UTS_round = [ // slotted, phillips diff --git a/shapes2d.scad b/shapes2d.scad index eb81e7a..a1c595c 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1079,7 +1079,7 @@ 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() +/// See Also: jittered_poly() /// Usage: /// jpath = _path_add_jitter(path, [dist], [closed=]); /// Description: @@ -1095,7 +1095,7 @@ module star(n, r, ir, d, or, od, id, step, realign=false, align_tip, align_pit, /// Example(3D): /// d = 100; h = 75; quadsize = 5; /// path = pentagon(d=d); -/// spath = subdivide_long_segments(path, quadsize, closed=true); +/// spath = subdivide_path(path, maxlen=quadsize, closed=true); /// jpath = _path_add_jitter(spath, closed=true); /// linear_extrude(height=h, twist=72, slices=h/quadsize) /// polygon(jpath); @@ -1115,7 +1115,7 @@ function _path_add_jitter(path, dist=1/512, closed=true) = // Module: jittered_poly() // Topics: Extrusions -// See Also: subdivide_long_segments() +// See Also: subdivide_path() // Usage: // jittered_poly(path, [dist]); // Description: @@ -1128,7 +1128,7 @@ function _path_add_jitter(path, dist=1/512, closed=true) = // Example: // d = 100; h = 75; quadsize = 5; // path = pentagon(d=d); -// spath = subdivide_long_segments(path, quadsize, closed=true); +// spath = subdivide_path(path, maxlen=quadsize, closed=true); // linear_extrude(height=h, twist=72, slices=h/quadsize) // jittered_poly(spath); module jittered_poly(path, dist=1/512) { diff --git a/skin.scad b/skin.scad index a92a3c4..6dd9a69 100644 --- a/skin.scad +++ b/skin.scad @@ -45,7 +45,7 @@ // Duplicating vertices allows two distinct points in one polygon to connect to a single point // in the other one, creating // triangular faces. You can adjust non-matching polygons yourself -// either by resampling them using `subdivide_path` or by duplicating vertices using +// either by resampling them using {{subdivide_path()}} or by duplicating vertices using // `repeat_entries`. It is OK to pass a polygon that has the same vertex repeated, such as // a square with 5 points (two of which are identical), so that it can match up to a pentagon. // Such a combination would create a triangular face at the location of the duplicated vertex. @@ -69,7 +69,7 @@ // on each edge, but if you refine a profile with the "length" method you will get new points // distributed around the profile based on length, so small segments will get fewer new points than longer ones. // A uniform division may be impossible, in which case the code computes an approximation, which may result -// in arbitrary distribution of extra points. See `subdivide_path` for more details. +// in arbitrary distribution of extra points. See {{subdivide_path()}} for more details. // Note that when dealing with continuous curves it is always better to adjust the // sampling in your code to generate the desired sampling rather than using the `refine` argument. // . @@ -84,7 +84,7 @@ // right shapes but the point lists don't start from points that you want aligned in your skinned // polyhedron. You can correct this yourself using `reindex_polygon`, or you can use the "reindex" // method which will look for the index choice that will minimize the length of all of the edges -// in the polyhedron---in will produce the least twisted possible result. This algorithm has quadratic +// in the polyhedron—it will produce the least twisted possible result. This algorithm has quadratic // run time so it can be slow with very large profiles. // . // When the profiles are incommensurate, the "direct" and "reindex" resample them to match. As noted above, @@ -808,7 +808,7 @@ module spiral_sweep(poly, h, r, turns=1, higbee, center, r1, r2, d, d1, d2, higb // The twist is normally spread uniformly along your shape based on the path length. If you set `twist_by_length` to // false then the twist will be uniform based on the point count of your path. Twisted shapes will produce twisted // faces, so if you want them to look good you should use lots of points on your path and also lots of points on the -// shape. If your shape is a simple polygon, use {{subdivide_path()}} or {{subdivide_long_segments()}} to increase +// shape. If your shape is a simple polygon, use {{subdivide_path()}} to increase // the number of points. // . // As noted above, the sweep process has an ambiguity regarding the twist. For 2D paths it is easy to resolve this @@ -835,7 +835,7 @@ module spiral_sweep(poly, h, r, turns=1, higbee, center, r1, r2, d, d1, d2, higb // . // The "natural" method works by computing the Frenet frame at each point on the path. This is defined by the tangent to the curve and // the normal which lies in the plane defined by the curve at each point. This normal points in the direction of curvature of the curve. -// The result is a very well behaved set of shape positions without any unexpected twisting---as long as the curvature never falls to zero. At a +// The result is a very well behaved set of shape positions without any unexpected twisting—as long as the curvature never falls to zero. At a // point of zero curvature (a flat point), the curve does not define a plane and the natural normal is not defined. Furthermore, even if // you skip over this troublesome point so the normal is defined, it can change direction abruptly when the curvature is zero, leading to // a nasty twist and an invalid model. A simple example is a circular arc joined to another arc that curves the other direction. Note @@ -1501,7 +1501,7 @@ module sweep(shape, transforms, closed=false, caps, style="min_edge", convexity= // profiles = profiles to operate on // slices = number of slices to insert between each pair of profiles. May be a vector // numpoints = number of points after sampling. -// method = method used for calling `subdivide_path`, either `"length"` or `"segment"`. Default: `"length"` +// method = method used for calling {{subdivide_path()}}, either `"length"` or `"segment"`. Default: `"length"` // closed = the first and last profile are connected. Default: false function subdivide_and_slice(profiles, slices, numpoints, method="length", closed=false) = let( @@ -1598,7 +1598,7 @@ function _smooth(data,len,closed=false,angle=false) = // Arguments: // rotlist = list of rotation operators in 3d to resample // n = Number of rotations to produce as output when method is "length" or number for each transformation if method is "count". Can be a vector when method is "count" -// -- +// --- // method = sampling method, either "length" or "count" // twist = scalar or vector giving twist to add overall or at each rotation. Default: none // scale = scalar or vector giving scale factor to add overall or at each rotation. Default: none diff --git a/std.scad b/std.scad index ffdd7e1..086c6a2 100644 --- a/std.scad +++ b/std.scad @@ -20,10 +20,10 @@ include include include include +include include include include -include include include include diff --git a/tests/test_paths.scad b/tests/test_paths.scad index 6bd3252..08d4097 100644 --- a/tests/test_paths.scad +++ b/tests/test_paths.scad @@ -165,13 +165,9 @@ module test_subdivide_path(){ 0.404508497187]]); assert_approx(subdivide_path([[0,0,0],[2,0,1],[2,3,2]], 12), [[0, 0, 0], [2/3, 0, 1/3], [4/3, 0, 2/3], [2, 0, 1], [2, 0.75, 1.25], [2, 1.5, 1.5], [2, 2.25, 1.75], [2, 3, 2], [1.6, 2.4, 1.6], [1.2, 1.8, 1.2], [0.8, 1.2, 0.8], [0.4, 0.6, 0.4]]); -} -test_subdivide_path(); - -module test_subdivide_long_segments(){ path = pentagon(d=100); - spath = subdivide_long_segments(path, 10, closed=true); + spath = subdivide_path(path, maxlen=10, closed=true); assert_approx(spath, [[50, 0], [44.2418082865, -7.92547096913], [38.4836165729, -15.8509419383], [32.7254248594, -23.7764129074], [26.9672331458, @@ -189,6 +185,11 @@ module test_subdivide_long_segments(){ 23.7764129074], [38.4836165729, 15.8509419383], [44.2418082865, 7.92547096913]]); } +test_subdivide_path(); + + +module test_subdivide_long_segments(){ +} test_subdivide_long_segments(); diff --git a/transforms.scad b/transforms.scad index 53cfc1f..085bd05 100644 --- a/transforms.scad +++ b/transforms.scad @@ -1326,13 +1326,13 @@ function is_2d_transform(t) = // z-parameters are zero, except we allow t[2][ // Topics: Affine, Matrices, Transforms // Description: // Applies the specified transformation matrix `transform` to a point, point list, bezier patch or VNF. -// When `points` contains 2D or 3D points the transform matrix may be a 4x4 affine matrix or a 3x4 matrix--- -// the 4x4 matrix with its final row removed. When the data is 2D the matrix must not operate on the Z axis, +// When `points` contains 2D or 3D points the transform matrix may be a 4x4 affine matrix or a 3x4 +// matrix—the 4x4 matrix with its final row removed. When the data is 2D the matrix must not operate on the Z axis, // except possibly by scaling it. When points contains 2D data you can also supply the transform as // a 3x3 affine transformation matrix or the corresponding 2x3 matrix with the last row deleted. // . // Any other combination of matrices will produce an error, including acting with a 2D matrix (3x3) on 3D data. -// The output of apply is always the same dimension as the input---projections are not supported. +// The output of apply is always the same dimension as the input—projections are not supported. // Arguments: // transform = The 2D (3x3 or 2x3) or 3D (4x4 or 3x4) transformation matrix to apply. // points = The point, point list, bezier patch, or VNF to apply the transformation to.