From 564db53c519fcd0e605dbe3e1dc6a0b110e0026f Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Tue, 10 Dec 2024 23:36:03 -0800 Subject: [PATCH 1/4] Replaced vnf_tri_array() with additional examples, added example to vnf_from_polygons() --- vnf.scad | 242 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 183 insertions(+), 59 deletions(-) diff --git a/vnf.scad b/vnf.scad index c94b892..76669a7 100644 --- a/vnf.scad +++ b/vnf.scad @@ -237,21 +237,27 @@ function vnf_vertex_array( // Function: vnf_tri_array() -// Synopsis: Returns a VNF from an array of points. +// Synopsis: Returns a VNF from an array of points. The array need not be rectangular. // SynTags: VNF // Topics: VNF Generators, Lists -// See Also: vnf_vertex_array(), vnf_join(), vnf_from_polygons(), vnf_from_region() +// See Also: vnf_vertex_array(), vnf_join(), vnf_from_polygons(), vnf_merge_points() // Usage: -// vnf = vnf_tri_array(points, [row_wrap], [reverse]) +// vnf = vnf_tri_array(points, [row_wrap], [reverse], [col_wrap], [caps], [cap1], [cap2]) // Description: -// Produces a VNF from an array of points where each row length can differ from the adjacent rows by up to 2 in length. This enables -// the construction of triangular VNF patches. The resulting VNF can be wrapped along the rows by setting `row_wrap` to true. -// You cannot wrap columns: if you need to do that you'll need to merge two VNF arrays that share edges. Degenerate faces -// are not included in the output, but if this results in unused vertices they will still appear in the output. +// Produces a VNF from an array of points where each row length can differ from the adjacent rows by any amount. This enables the construction of triangular or even irregular VNF patches. The resulting VNF can be wrapped along the rows by setting `row_wrap` to true, and wrapped along columns by setting `col_wrap` to true. It is possible to do both at once. +// If `row_wrap` is false, end caps can be generated across either the top and bottom rows. +// . +// The algorithm starts with the first point on each row and recursively walks around finding the minimum-length edge to make each new triangle face. This may result in several triangles being connected to one vertex. When triangulating two rows that happen to be equal length, the result is equivalent to {{vnf_vertex_array()}} using the "min_edge" style. If you already have a rectangular vertex list (equal length rows), you should use `vnf_vertex_array()` if you need a different triangulation style. +// . +// If you need to merge two VNF arrays that share edges using `vnf_join()` you can remove the duplicated vertices using `vnf_merge_points()`. // Arguments: -// 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 +// points = List of point lists for each row. +// row_wrap = If true, then add faces connecting the first row and last row. The rows may be unequal length. +// reverse = If true, reverses the direction of the faces. +// col_wrap = If true, then add faces connecting the first column and last column. +// caps = Close both first and last rows with end caps, if `row_wrap=false`. +// cap1 = Close first row with an end cap, if `row_wrap=false`. +// cap2 = Close last row with an end cap, if `row_wrap=false`. // Example(3D,NoAxes): 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); @@ -276,61 +282,164 @@ function vnf_vertex_array( // vnf_tri_array(pts2)]); // color("green")vnf_wireframe(vnf,width=0.1); // vnf_polyhedron(vnf); -// Example(3D,NoAxes): Point count can change irregularly -// lens = [10,9,7,5,6,8,8,10]; +// Example(3D,NoAxes): The number of points per row can change irregularly by any amount. +// lens = [10,9,8,6,4,8,2,5,3,10,4]; // pts = [for(y=idx(lens)) lerpn([-lens[y],y,y],[lens[y],y,y],lens[y])]; // vnf = vnf_tri_array(pts); // vnf_wireframe(vnf,width=0.1); // color("red")move_copies(flatten(pts)) sphere(r=.15,$fn=9); -function vnf_tri_array(points, row_wrap=false, reverse=false) = +// Example(3D,NoAxes,ThrownTogether): A simple open-ended shape made from a series of arcs with different number of vertices at different elevations. Clockwise from upper left: (1) no row or column wrapping, (2) wrap rows only, (3) wrap rows and columns, (4) wrap columns only, with caps. The `reverse=true` parameter is needed because arcs generate counterclockwise, while `vnf_tri_array()` expects clockwise, as with polygons. +// polypoints = [12, 16, 25, 26, 25, 18, 10]; +// arc_elev = [1, 0, 6, 8, 10, 15, 14]; +// rows = [ +// for(i = [0:len(arc_elev)-1]) +// path3d(arc(r=polypoints[i]*2, angle=280, $fn=polypoints[i]), 5*arc_elev[i]) +// ]; +// vnf1 = vnf_tri_array(rows, reverse=true); // no row or column wrapping +// translate([-60,60,0]) { +// vnf_polyhedron(vnf1); +// color("green") vnf_wireframe(vnf1); +// } +// vnf2 = vnf_tri_array(rows, reverse=true, row_wrap=true); // wrap rows only +// translate([60,60,0]) { +// vnf_polyhedron(vnf2); +// color("green") vnf_wireframe(vnf2); +// } +// vnf3 = vnf_tri_array(rows, reverse=true, row_wrap=true, col_wrap=true); // wrap rows and columns +// translate([60,-60,0]) { +// vnf_polyhedron(vnf3); +// color("green") vnf_wireframe(vnf3); +// } +// vnf4 = vnf_tri_array(rows, reverse=true, col_wrap=true, caps=true); // wrap columns only, with caps +// translate([-60,-60,0]) { +// vnf_polyhedron(vnf4); +// color("green") vnf_wireframe(vnf4); +// } +// Example(3D,NoAxes,Edges): Model of a cymbal with roughly same-size facets, using a different number of points for each concentric ring of vertices. +// include +// bez = [ +// [[0,22], [35,22], [30,0], [80,14], [102,0]], //top +// [[99,-1], [79,13], [29,-1], [34,21], [-1,21]] // bottom +// ]; +// points = [ +// for(b=bez) +// for(u=[0.01:0.04:1]) let( +// bzp = bezier_points(b,u), +// r = bzp[0], +// n = max(3, round(360 / (6/r * 180/PI))) +// ) path3d(regular_ngon(n, r=r), bzp[1]) +// ]; +// vnf = vnf_tri_array(points, reverse=true, col_wrap=true, caps=true); +// color("brown") difference() { +// vnf_polyhedron(vnf); +// cylinder(30, d=8); +// } + +function vnf_tri_array(points, row_wrap=false, reverse=false, col_wrap=false, caps=false, cap1=false, cap2=false) = + assert(!(row_wrap && (caps || cap1 || cap2)), "Caps cannot exist when row_wrap=true") let( - lens = [for(row=points) len(row)], - rowstarts = [0,each cumsum(lens)], - faces = - [for(i=[0:1:len(points) - 1 - (row_wrap ? 0 : 1)]) each - let( - rowstart = rowstarts[i], - nextrow = select(rowstarts,i+1), - delta = select(lens,i+1)-lens[i] - ) - delta == 0 ? - [for(j=[0:1:lens[i]-2]) reverse ? [j+rowstart+1, j+rowstart, j+nextrow] : [j+rowstart, j+rowstart+1, j+nextrow], - for(j=[0:1:lens[i]-2]) reverse ? [j+rowstart+1, j+nextrow, j+nextrow+1] : [j+rowstart+1, j+nextrow+1, j+nextrow]] : - delta == 1 ? - [for(j=[0:1:lens[i]-2]) reverse ? [j+rowstart+1, j+rowstart, j+nextrow+1] : [j+rowstart, j+rowstart+1, j+nextrow+1], - for(j=[0:1:lens[i]-1]) reverse ? [j+rowstart, j+nextrow, j+nextrow+1] : [j+rowstart, j+nextrow+1, j+nextrow]] : - delta == -1 ? - [for(j=[0:1:lens[i]-3]) reverse ? [j+rowstart+1, j+nextrow, j+nextrow+1]: [j+rowstart+1, j+nextrow+1, j+nextrow], - for(j=[0:1:lens[i]-2]) reverse ? [j+rowstart+1, j+rowstart, j+nextrow] : [j+rowstart, j+rowstart+1, j+nextrow]] : - let(count = floor((lens[i]-1)/2)) - delta == 2 ? - [ - for(j=[0:1:count-1]) reverse ? [j+rowstart+1, j+rowstart, j+nextrow+1] : [j+rowstart, j+rowstart+1, j+nextrow+1], // top triangles left - for(j=[count:1:lens[i]-2]) reverse ? [j+rowstart+1, j+rowstart, j+nextrow+2] : [j+rowstart, j+rowstart+1, j+nextrow+2], // top triangles right - for(j=[0:1:count]) reverse ? [j+rowstart, j+nextrow, j+nextrow+1] : [j+rowstart, j+nextrow+1, j+nextrow], // bot triangles left - for(j=[count+1:1:select(lens,i+1)-2]) reverse ? [j+rowstart-1, j+nextrow, j+nextrow+1] : [j+rowstart-1, j+nextrow+1, j+nextrow], // bot triangles right - ] : - delta == -2 ? - [ - for(j=[0:1:count-2]) reverse ? [j+nextrow, j+nextrow+1, j+rowstart+1] : [j+nextrow, j+rowstart+1, j+nextrow+1], - for(j=[count-1:1:lens[i]-4]) reverse ? [j+nextrow,j+nextrow+1,j+rowstart+2] : [j+nextrow,j+rowstart+2, j+nextrow+1], - for(j=[0:1:count-1]) reverse ? [j+nextrow, j+rowstart+1, j+rowstart] : [j+nextrow, j+rowstart, j+rowstart+1], - for(j=[count:1:select(lens,i+1)]) reverse ? [ j+nextrow-1, j+rowstart+1, j+rowstart]: [ j+nextrow-1, j+rowstart, j+rowstart+1], - ] : - assert(false,str("Unsupported row length difference of ",delta, " between row ",i," and ",(i+1)%len(points))) - ], - verts = flatten(points), - culled_faces= - [for(face=faces) - if (norm(verts[face[0]]-verts[face[1]])>EPSILON && - norm(verts[face[1]]-verts[face[2]])>EPSILON && - norm(verts[face[2]]-verts[face[0]])>EPSILON) - face - ] - ) - [flatten(points), culled_faces]; + plen = len(points), + // append first vertex of each polygon to its end if wrapping columns + st = col_wrap ? [ + for(i=[0:plen-1]) + points[i][0] != points[i][len(points[i])-1] + ? concat(points[i], [points[i][0]]) + : points[i] + ] : points, + addcol = col_wrap ? len(st[0])-len(points[0]) : 0, + rowstarts = [ for(i=[0:plen-1]) len(st[i]) ], + capfirst = caps ? true : cap1, + caplast = caps ? true : cap2, + pcumlen = [0, each cumsum(rowstarts)], + + faces = flatten([ + // close first end + if (capfirst) + if (reverse) [[ for(i=[0:rowstarts[0]-1-addcol]) i ]] + else [[ for(i=[rowstarts[0]-1-addcol:-1:0]) i ]], + // triangulate between the two polygons + for(i = [0:plen-2+(row_wrap?1:0)]) let(j = (i+1)%plen) + _lofttri(st[i], st[j], pcumlen[i], pcumlen[j], rowstarts[i], rowstarts[j], reverse), + // close up the last end + if (caplast) + if (reverse) [[ for(i=[pcumlen[plen]-1-addcol:-1:pcumlen[plen-1]]) i ]] + else [[ for(i=[pcumlen[plen-1]:pcumlen[plen]-1-addcol]) i ]] + ]), + vnf = [flatten(st), faces] + ) col_wrap ? vnf_merge_points(vnf) : vnf; +/* +Recursively triangulate between two 3D paths (which can be different +in length by any amount), starting at index 0 and generate a list of +triangles with minimum new-side length. +The first side of the first triangle always connects the two first +vertices of each path. +To triangulate between two closed paths, the first and last vertices +must be the same. +Parameters: + p1 = first path, an array of [x,y,z] vertices + p2 = second path, an array of [x,y,z] vertices + i1offset = index offset of first vertex in the first path + (sum of any prior path lengths) + i2offset = index offset of first vertex in the second path + (sum of any prior path lengths) + n1 = number of vertices in first path + n2 = number of vertices in second path + reverse = if true, assume a polygon path goes around + counterclockwise with respect to the direction from + p1 to p2 (right hand rule), clockwise if false +Other parameters are for internal use: + trilist[] = array of triangles to return + i1 = vertex index on p1 of the next triangle + i2 = vertex index on p2 of the next triangle +(next triangle vertex found can be on either p1 or p2, depending +on which triangle is smaller.) + +Returns an array of triangles using vertex indices offset by +i1offset and i2offset +*/ +function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], i1=0, i2=0) = n1!=n2 ? + // unequal row lengths + let( + t1 = i1 < n1 ? i1+1 : n1, // test point 1 + t2 = i2 < n2 ? i2+1 : n2, // test point 2 + d12 = t2>=n2 ? 9e+9 : norm(p2[t2]-p1[i1]), // distance from i1 to t2 + d21 = t1>=n1 ? 9e+9 : norm(p1[t1]-p2[i2]), // distance from i2 to t1 + triangle = reverse ? + [i1offset+i1, i2offset+i2, d12=n1 && t2>=n2 ? trilist : + _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse, concat(trilist, [triangle]), d12=n ? 9e+9 : norm(p2[t]-p1[i]), // distance from p1 to new p2 + d21 = t>=n ? 9e+9 : norm(p1[t]-p2[i]), // distance from p2 to new p1 + triangle1 = reverse ? + [i1offset+i, i2offset+i, d12=n ? trilist : + _lofttri_eq(p1, p2, i1offset, i2offset, n, reverse, concat(trilist, [triangle1, triangle2]), t); + +/* +function _lofttri_eq(p1, p2, i1offset, i2offset, n, reverse=false, trilist=[], i=0) = let( + t = i < n ? i+1 : n, // test point + d12 = t>=n ? 9e+9 : norm(p2[t]-p1[i]), // distance from p1 to new p2 + d21 = t>=n ? 9e+9 : norm(p1[t]-p2[i]), // distance from p2 to new p1 + triangle1 = reverse ? + [i1offset+i, i2offset+i, d12=n ? trilist : + _lofttri_eq(p1, p2, i1offset, i2offset, n, reverse, concat(trilist, [triangle1, triangle2]), t); +*/ // Function: vnf_join() // Synopsis: Returns a single VNF structure from a list of VNF structures. @@ -436,7 +545,22 @@ function vnf_join(vnfs) = // Arguments: // polygons = The list of 3D polygons to turn into a VNF // fast = Set to true to skip area and coplanarity checks for increased speed. Default: false -// eps = Polygons with area small than this are discarded. Default: EPSILON +// eps = Polygons with area smaller than this are discarded. Default: EPSILON +// Example(3D,VPR=[60,0,40]): Construction of a dodecahedron from pentagon faces. +// dihedral = 2*atan(PHI); // dodecahedron face dihedral +// rpenta = 10; // pentagon face radius +// edge = 2*rpenta*sin(36); // edge length +// inrad = 0.5*edge * PHI*PHI/sqrt(3-PHI); // inner radius +// face3d = path3d(pentagon(rpenta), inrad); // single face +// facepoints = [ +// face3d, +// for(a=[36:72:360]) zrot(a, yrot(180-dihedral, face3d)), +// for(a=[36:72:360]) zrot(a, yrot(360-dihedral, face3d)), +// yrot(180, face3d) +// ]; +// vnf = vnf_from_polygons(facepoints, fast=true); +// vnf_polyhedron(vnf); + function vnf_from_polygons(polygons,fast=false,eps=EPSILON) = assert(is_list(polygons) && is_path(polygons[0]),"Input should be a list of polygons") let( From 1ed79d4cac846f5b4b614cac9a039d7960e01aeb Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Wed, 11 Dec 2024 07:26:11 -0800 Subject: [PATCH 2/4] Restored critical line in vnf_tri_array that somehow got removed while cleaning up comments --- vnf.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vnf.scad b/vnf.scad index 76669a7..f61de3f 100644 --- a/vnf.scad +++ b/vnf.scad @@ -413,7 +413,7 @@ function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse, concat(trilist, [triangle]), d12=n ? 9e+9 : norm(p2[t]-p1[i]), // distance from p1 to new p2 d21 = t>=n ? 9e+9 : norm(p1[t]-p2[i]), // distance from p2 to new p1 From 2a2062231af5504d6e5c237f2c914f85cc2ff019 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Wed, 11 Dec 2024 07:43:33 -0800 Subject: [PATCH 3/4] Fixed another issue from inadvertent undo operation, retested --- vnf.scad | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vnf.scad b/vnf.scad index f61de3f..e937b1e 100644 --- a/vnf.scad +++ b/vnf.scad @@ -318,8 +318,8 @@ function vnf_vertex_array( // Example(3D,NoAxes,Edges): Model of a cymbal with roughly same-size facets, using a different number of points for each concentric ring of vertices. // include // bez = [ -// [[0,22], [35,22], [30,0], [80,14], [102,0]], //top -// [[99,-1], [79,13], [29,-1], [34,21], [-1,21]] // bottom +// [[0,24], [35,24], [30,0], [80,15], [102,0]], //top +// [[99,-1], [79,14], [29,-1], [34,23], [-1,23]] // bottom // ]; // points = [ // for(b=bez) @@ -413,7 +413,7 @@ function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse, concat(trilist, [triangle]), d12=n ? 9e+9 : norm(p2[t]-p1[i]), // distance from p1 to new p2 d21 = t>=n ? 9e+9 : norm(p1[t]-p2[i]), // distance from p2 to new p1 @@ -424,7 +424,7 @@ function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], [i2offset+t, i1offset+t, d12=n ? trilist : - _lofttri_eq(p1, p2, i1offset, i2offset, n, reverse, concat(trilist, [triangle1, triangle2]), t); + _lofttri(p1, p2, i1offset, i2offset, n, n, reverse, concat(trilist, [triangle1, triangle2]), t, t); /* function _lofttri_eq(p1, p2, i1offset, i2offset, n, reverse=false, trilist=[], i=0) = let( From fe98042255acd96a82b14329e13c61eb73396ff3 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Wed, 11 Dec 2024 09:28:25 -0800 Subject: [PATCH 4/4] Changed height and view angle of cymbal model, removed redundant comment block --- vnf.scad | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/vnf.scad b/vnf.scad index e937b1e..2fc51a9 100644 --- a/vnf.scad +++ b/vnf.scad @@ -315,11 +315,11 @@ function vnf_vertex_array( // vnf_polyhedron(vnf4); // color("green") vnf_wireframe(vnf4); // } -// Example(3D,NoAxes,Edges): Model of a cymbal with roughly same-size facets, using a different number of points for each concentric ring of vertices. +// Example(3D,NoAxes,Edges,VPR=[65,0,25]): Model of a cymbal with roughly same-size facets, using a different number of points for each concentric ring of vertices. // include // bez = [ -// [[0,24], [35,24], [30,0], [80,15], [102,0]], //top -// [[99,-1], [79,14], [29,-1], [34,23], [-1,23]] // bottom +// [[0,26], [35,26], [29,0], [80,16], [102,0]], //top +// [[99,-1], [79,15], [28,-1], [34,25], [-1,25]] // bottom // ]; // points = [ // for(b=bez) @@ -426,20 +426,6 @@ function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], ) t>=n ? trilist : _lofttri(p1, p2, i1offset, i2offset, n, n, reverse, concat(trilist, [triangle1, triangle2]), t, t); -/* -function _lofttri_eq(p1, p2, i1offset, i2offset, n, reverse=false, trilist=[], i=0) = let( - t = i < n ? i+1 : n, // test point - d12 = t>=n ? 9e+9 : norm(p2[t]-p1[i]), // distance from p1 to new p2 - d21 = t>=n ? 9e+9 : norm(p1[t]-p2[i]), // distance from p2 to new p1 - triangle1 = reverse ? - [i1offset+i, i2offset+i, d12=n ? trilist : - _lofttri_eq(p1, p2, i1offset, i2offset, n, reverse, concat(trilist, [triangle1, triangle2]), t); -*/ // Function: vnf_join() // Synopsis: Returns a single VNF structure from a list of VNF structures.