From 51af394c240e7c337f532d0fe972948aff893bb2 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Wed, 4 Mar 2020 20:24:00 -0500 Subject: [PATCH] Added force_list, path_to_bezier, smooth_path, associate_vertices, improved skin and sweep error handling. Allow path_sweep to take a 2d path. --- arrays.scad | 9 ++++++ beziers.scad | 27 ++++++++++++++-- math.scad | 2 +- paths.scad | 5 +-- rounding.scad | 27 ++++++++++++++++ skin.scad | 85 +++++++++++++++++++++++++++++++++++++++++++++------ 6 files changed, 140 insertions(+), 15 deletions(-) diff --git a/arrays.scad b/arrays.scad index d04bd13..2c0f36b 100644 --- a/arrays.scad +++ b/arrays.scad @@ -851,6 +851,15 @@ function enumerate(l,idx=undef) = [for (i=[0:1:len(l)-1]) concat([i], [for (j=idx) l[i][j]])]; +// Function: force_list() +// Usage: +// list = force_list(value) +// Description: +// If value is a list returns value, otherwise returns [value]. Makes it easy to +// treat a scalar input consistently as a singleton list along with list inputs. +function force_list(value) = is_list(value) ? value : [value]; + + // Function: pair() // Usage: // pair(v) diff --git a/beziers.scad b/beziers.scad index 801afde..3375402 100644 --- a/beziers.scad +++ b/beziers.scad @@ -9,8 +9,8 @@ ////////////////////////////////////////////////////////////////////// -include - +include +include // Section: Terminology // **Polyline**: A series of points joined by straight line segements. @@ -318,6 +318,29 @@ function bezier_polyline(bezier, splinesteps=16, N=3) = let( ); +// Function: path_to_bezier() +// Usage: +// path_to_bezier(path,[tangent],[closed]); +// Description: +// Given an input path and optional path of tangent vectors, computes a cubic (degree 3) bezier path that passes +// through every point on the input path and matches the tangent vectors. If you do not supply +// the tangent it will be computed using path_tangents. If the path is closed specify this +// by setting closed=true. +// Arguments: +// path = path of points to define the bezier +// tangents = optional list of tangent vectors at every point +// closed = set to true for a closed path. Default: false +function path_to_bezier(path, tangent, closed=false) = + assert(is_path(path,dim=undef),"Input path is not a valid path") + assert(is_undef(tangent) || is_path(tanget,dim=len(path[0])),"Tangent must be a path of the same dimension as the input path") + let( + tangent = is_def(tangent)? tangent : path_tangents(path, closed=closed), + lastpt = len(path) - (closed?0:1) + ) + [for(i=[0:lastpt-1]) each [path[i], path[i]+tangent[i], select(path,i+1)-select(tangent,i+1)], + select(path,lastpt)]; + + // Function: fillet_path() // Usage: diff --git a/math.scad b/math.scad index f0637a7..ec4898e 100644 --- a/math.scad +++ b/math.scad @@ -412,7 +412,7 @@ function _lcmlist(a) = function lcm(a,b=[]) = !is_list(a) && !is_list(b) ? _lcm(a,b) : let( - arglist = concat((is_list(a)?a:[a]), (is_list(b)?b:[b])) + arglist = concat(force_list(a),force_list(b)) ) assert(len(arglist)>0,"invalid call to lcm with empty list(s)") _lcmlist(arglist); diff --git a/paths.scad b/paths.scad index c496723..8b2cf79 100644 --- a/paths.scad +++ b/paths.scad @@ -46,7 +46,7 @@ function is_path(list, dim=[2,3], fast=false) = fast? is_list(list) && is_vector(list[0],fast=true) : is_list(list) && is_list(list[0]) && len(list)>1 && let( d = len(list[0]) ) - (is_undef(dim) || in_list(d, is_list(dim)?dim:[dim]) ) && + (is_undef(dim) || in_list(d, force_list(dim))) && is_list_of(list, replist(0,d)); @@ -287,7 +287,7 @@ function path_closest_point(path, pt) = // The returns vectors will be normalized to length 1. function path_tangents(path, closed=false) = assert(is_path(path)) - [for(t=deriv(path)) unit(t)]; + [for(t=deriv(path,closed=closed)) unit(t)]; // Function: path_normals() @@ -862,6 +862,7 @@ module path_extrude(path, convexity=10, clipsize=100) { // polyline = [for (a=[0:30:210]) 10*[cos(a), sin(a), sin(a)]]; // trace_polyline(polyline, showpts=true, size=0.5, color="lightgreen"); module trace_polyline(pline, closed=false, showpts=false, N=1, size=1, color="yellow") { + assert(is_path(pline),"Input pline is not a path"); sides = segs(size/2); pline = closed? close_path(pline) : pline; if (showpts) { diff --git a/rounding.scad b/rounding.scad index 2088e92..5015b24 100644 --- a/rounding.scad +++ b/rounding.scad @@ -407,6 +407,33 @@ function _rounding_offsets(edgespec,z_dir=1) = +// Function: smooth_path() +// Usage: +// smooth_path(path, [tangent], [splinesteps], [closed] +// Description: +// Smooths the input path using a cubic spline. Every segment of the path will be replaced by a cubic curve +// with `splinesteps` points. The cubic interpolation will pass through every input point on the path +// and will match the tangents at every point. If you do not specify tangents they will be computed using +// path_tangents(). See also path_to_bezier(). +// Arguments: +// path = path to smooth +// tangents = tangent vectors of the path +// splinesteps = number of points to insert between the path points. Default: 10 +// closed = set to true for a closed path. Default: false +// Example(2D): Original path in green, smoothed path in yellow: +// color("green")stroke(square(4), width=0.1); +// stroke(smooth_path(square(4)), width=0.1); +// Example(2D): Closing the path changes the end tangents +// polygon(smooth_path(square(4), closed=true)); +// Example(FlatSpin): Works on 3d paths as well +// path = [[0,0,0],[3,3,2],[6,0,1],[9,9,0]]; +// trace_polyline(smooth_path(path),size=.3); +function smooth_path(path, tangent, splinesteps=10, closed=false) = + let( + bez = path_to_bezier(path, tangent=tangent, closed=closed) + ) + bezier_polyline(bez,splinesteps=splinesteps); + // Module: offset_sweep() // diff --git a/skin.scad b/skin.scad index 62a4a0a..c4fba71 100644 --- a/skin.scad +++ b/skin.scad @@ -324,6 +324,7 @@ module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed= function skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z) = + assert(is_def(slices),"The slices argument must be specified.") assert(is_list(profiles) && len(profiles)>1, "Must provide at least two profiles") let( bad = [for(i=idx(profiles)) if (!(is_path(profiles[i]) && len(profiles[i])>2)) i]) assert(len(bad)==0, str("Profiles ",bad," are not a paths or have length less than 3")) @@ -715,6 +716,71 @@ function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) = zero_cross[min_index(d)]; +// Function: associate_vertices() +// Usage: +// associate_vertices(polygons, split) +// Description: +// Takes as input a list of polygons and duplicates specified vertices in each polygon in the list through the series so +// that the input can be passed to `skin()`. This allows you to decide how the vertices are linked up rather than accepting +// the automatically computed minimal distance linkage. However, the number of vertices in the polygons must not decrease in the list. +// The output is a list of polygons that all have the same number of vertices with some duplicates. You specify the vertix splitting +// using the `split` which is a list where each entry corresponds to a polygon: split[i] is a value or list specfying which vertices in polygon i to split. +// Give the empty list if you don't want a split for a particular polygon. If you list a vertex once then it will be split and mapped to +// two vertices in the next polygon. If you list it N times then N copies will be created to map to N+1 vertices in the next polygon. +// You must ensure that each mapping produces the correct number of vertices to exactly map onto every vertex of the next polygon. +// Note that if you split (only) vertex i of a polygon that means it will map to vertices i and i+1 of the next polygon. Vertex 0 will always +// map to vertex 0 and the last vertices will always map to each other, so if you want something different than that you'll need to reindex +// your polygons. +// Arguments: +// polygons = list of polygons to split +// split = list of lists of split vertices +// Example(FlatSpin): If you skin together a square and hexagon using the optimal distance method you get two triangular faces on opposite sides: +// sq = regular_ngon(4,side=2); +// hex = apply(rot(15),hexagon(side=2)); +// skin([sq,hex], slices=10, refine=10, method="distance", z=[0,4]); +// Example(FlatSpin): Using associate_vertices you can change the location of the triangular faces. Here they are connect to two adjacent vertices of the square: +// sq = regular_ngon(4,side=2); +// hex = apply(rot(15),hexagon(side=2)); +// skin(associate_vertices([sq,hex],[[1,2]]), slices=10, refine=10, sampling="segment", z=[0,4]); +// Example(FlatSpin): Here the two triangular faces connect to a single vertex on the square. Note that we had to rotate the hexagon to line them up because the vertices match counting forward, so in this case vertex 0 of the square matches to vertices 0, 1, and 2 of the hexagon. +// sq = regular_ngon(4,side=2); +// hex = apply(rot(60),hexagon(side=2)); +// skin(associate_vertices([sq,hex],[[0,0]]), slices=10, refine=10, sampling="segment", z=[0,4]); +// Example: This example shows several polygons, with only a single vertex split at each step: +// sq = regular_ngon(4,side=2); +// pent = pentagon(side=2); +// hex = hexagon(side=2); +// sep = regular_ngon(7,side=2); +// skin(associate_vertices([sq,pent,hex,sep], [1,3,4]) ,slices=10, refine=10, method="distance", z=[0,2,4,6]); +// Example: The polygons cannot shrink, so if you want to have decreasing polygons you'll need to concatenate multiple results. Note that it is perfectly ok to duplicate a profile as shown here, where the pentagon is duplicated: +// sq = regular_ngon(4,side=2); +// pent = pentagon(side=2); +// grow = associate_vertices([sq,pent],[1]); +// shrink = associate_vertices([sq,pent],[2]); +// skin(concat(grow, reverse(shrink)), slices=10, refine=10, method="distance", z=[0,2,2,4]); +function associate_vertices(polygons, split, curpoly=0) = + curpoly==len(polygons)-1 ? polygons : + let( + polylen = len(polygons[curpoly]), + cursplit = force_list(split[curpoly]), + fdsa= echo(cursplit=cursplit) + ) + assert(len(split)==len(polygons)-1,str(split,"Split list length mismatch: it has length ", len(split)," but must have length ",len(polygons)-1)) + assert(polylen<=len(polygons[curpoly+1]),str("Polygon ",curpoly," has more vertices than the next one.")) + assert(len(cursplit)+polylen == len(polygons[curpoly+1]), + str("Polygon ", curpoly, " has ", polylen, " vertices. Next polygon has ", len(polygons[curpoly+1]), + " vertices. Split list has length ", len(cursplit), " but must have length ", len(polygons[curpoly+1])-polylen)) + assert(max(cursplit)=0, + str("Split ",cursplit," at polygon ",curpoly," has invalid vertices. Must be in [0:",polylen-1,"]")) + len(cursplit)==0 ? associate_vertices(polygons,split,curpoly+1) : + let( + splitindex = sort(concat(list_range(polylen), cursplit)), + newpoly = [for(i=[0:len(polygons)-1]) i<=curpoly ? select(polygons[i],splitindex) : polygons[i]] + ) + associate_vertices(newpoly, split, curpoly+1); + + + // Function&Module: sweep() // Usage: sweep(shape, transformations, [closed], [caps]) // Description: @@ -759,18 +825,16 @@ function _find_one_tangent(curve, edge, curve_offset=[0,0,0], closed=true) = // sweep(shape, concat(outside,inside)); function sweep(shape, transformations, closed=false, caps) = + assert(is_list_of(transformations, ident(4)), "Input transformations must be a list of numeric 4x4 matrices in sweep") + assert(is_path(shape,2), "Input shape must be a 2d path") let( - tdim = array_dim(transformations), - shapedim = array_dim(shape), 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 ) - assert(len(tdim)==3 && tdim[1]==4 && tdim[2]==4, "transformations must be a list of 4x4 matrices in sweep") - assert(tdim[0]>1, "transformation must be length 2 or more") - assert(len(shapedim)==2 && shapedim[0]>2, "shape must be a path of at least 3 points") - assert(shapedim[1]==2, "shape must be a path in 2-dimensions") + assert(len(transformations), "transformation must be length 2 or more") + assert(len(shape)>=3, "shape must be a path of at least 3 points") assert(capsOK, "caps must be boolean or a list of two booleans") assert(!closed || !caps, "Cannot make closed shape with caps") _skin_core([for(i=[0:len(transformations)-(closed?0:1)]) apply(transformations[i%len(transformations)],path3d(shape))],caps=fullcaps); @@ -783,7 +847,7 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) { // Function&Module: path_sweep() // Usage: path_sweep(shape, path, [method], [normal], [closed], [twist], [twist_by_length], [symmetry], [last_normal], [tangent], [relaxed], [caps], [convexity], [transforms]) // Description: -// Takes as input a 2d shape (specified as a point list) and a 3d path and constructs a polyhedron by sweeping the shape along the path. +// Takes as input a 2d shape (specified as a point list) and a 2d or 3d path and constructs a polyhedron by sweeping the shape along the path. // When run as a module returns the polyhedron geometry. When run as a function returns a VNF by default or if you set `transforms=true` then // it returns a list of transformations suitable as input to `sweep`. // @@ -858,7 +922,7 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) { // Example: Sweep along a clockwise elliptical arc, using "natural" method, which lines up the X axis of the shape with the direction of curvature. This means the X axis will point inward, so a counterclockwise arc gives: // ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; // elliptic_arc = xscale(2, p=arc($fn=64,angle=[0,180], r=30)); // Counter-clockwise -// path_sweep(ushape, path3d(elliptic_arc), method="natural"); +// path_sweep(ushape, elliptic_arc, method="natural"); // Example: Sweep along a clockwise elliptical arc, using "natural" method. If the curve is clockwise than the shape flips upside-down to align the X axis. // ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; // elliptic_arc = xscale(2, p=arc($fn=64,angle=[180,0], r=30)); // Clockwise @@ -1069,9 +1133,10 @@ function path_sweep(shape, path, method="incremental", normal, closed=false, twi assert(!closed || twist % (360/symmetry)==0, str("For a closed sweep, twist must be a multiple of 360/symmetry = ",360/symmetry)) assert(closed || symmetry==1, "symmetry must be 1 when closed is false") assert(is_integer(symmetry) && symmetry>0, "symmetry must be a positive integer") - assert(is_path(shape) && len(shape[0])==2, "shape must be a 2d path") - assert(is_path(path) && len(path[0])==3, "path must be a 3d path") + assert(is_path(shape,2), "shape must be a 2d path") + assert(is_path(path), "input path is not a path") let( + path = path3d(path), 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])),