From f2bb9bcb4ba99b495aa8f76c59e2f20eac7b1753 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sat, 19 Dec 2020 11:48:05 -0500 Subject: [PATCH] Added vnf_wireframe, tweaked reverse error message, --- arrays.scad | 2 +- hull.scad | 514 ++++++++++++++++++++++++++-------------------------- vnf.scad | 27 +++ 3 files changed, 285 insertions(+), 258 deletions(-) diff --git a/arrays.scad b/arrays.scad index db9a4c6..ff3d9d4 100644 --- a/arrays.scad +++ b/arrays.scad @@ -290,7 +290,7 @@ function list_range(n=undef, s=0, e=undef, step=undef) = // Example: // reverse([3,4,5,6]); // Returns [6,5,4,3] function reverse(x) = - assert(is_list(x)||is_string(x), "Input to reverse must be a list or string") + assert(is_list(x)||is_string(x), str("Input to reverse must be a list or string. Got: ",x)) let (elems = [ for (i = [len(x)-1 : -1 : 0]) x[i] ]) is_string(x)? str_join(elems) : elems; diff --git a/hull.scad b/hull.scad index 874e2c8..e2a6b3d 100644 --- a/hull.scad +++ b/hull.scad @@ -1,257 +1,257 @@ -////////////////////////////////////////////////////////////////////// -// LibFile: hull.scad -// Functions to create 2D and 3D convex hulls. -// To use, add the following line to the beginning of your file: -// ``` -// include -// include -// ``` -// Derived from Oskar Linde's Hull: -// - https://github.com/openscad/scad-utils -////////////////////////////////////////////////////////////////////// - - -// Section: Convex Hulls - - -// Function: hull() -// Usage: -// hull(points); -// Description: -// Takes a list of 2D or 3D points (but not both in the same list) and returns either the list of -// indexes into `points` that forms the 2D convex hull perimeter path, or the list of faces that -// form the 3d convex hull surface. Each face is a list of indexes into `points`. If the input -// points are co-linear, the result will be the indexes of the two extrema points. If the input -// points are co-planar, the results will be a simple list of vertex indices that will form a planar -// perimeter. Otherwise a list of faces will be returned, where each face is a simple list of -// vertex indices for the perimeter of the face. -// Arguments: -// points = The set of 2D or 3D points to find the hull of. -function hull(points) = - assert(is_path(points),"Invalid input to hull") - len(points[0]) == 2 - ? hull2d_path(points) - : hull3d_faces(points); - - -// Module: hull_points() -// Usage: -// hull_points(points, [fast]); -// Description: -// If given a list of 2D points, creates a 2D convex hull polygon that encloses all those points. -// If given a list of 3D points, creates a 3D polyhedron that encloses all the points. This should -// handle about 4000 points in slow mode. If `fast` is set to true, this should be able to handle -// far more. -// Arguments: -// points = The list of points to form a hull around. -// fast = If true, uses a faster cheat that may handle more points, but also may emit warnings that can stop your script if you have "Halt on first warning" enabled. Default: false -// Example(2D): -// pts = [[-10,-10], [0,10], [10,10], [12,-10]]; -// hull_points(pts); -// Example: -// pts = [for (phi = [30:60:150], theta = [0:60:359]) spherical_to_xyz(10, theta, phi)]; -// hull_points(pts); -module hull_points(points, fast=false) { - if (points) { - assert(is_list(points[0])); - if (fast) { - if (len(points[0]) == 2) { - hull() polygon(points=points); - } else { - extra = len(points)%3; - faces = concat( - [[for(i=[0:1:extra+2])i]], - [for(i=[extra+3:3:len(points)-3])[i,i+1,i+2]] - ); - hull() polyhedron(points=points, faces=faces); - } - } else { - perim = hull(points); - if (is_num(perim[0])) { - polygon(points=points, paths=[perim]); - } else { - polyhedron(points=points, faces=perim); - } - } - } -} - - -// Function: hull2d_path() -// Usage: -// hull2d_path(points) -// Description: -// Takes a list of arbitrary 2D points, and finds the convex hull polygon to enclose them. -// Returns a path as a list of indices into `points`. May return extra points, that are on edges of the hull. -// Example(2D): -// pts = [[-10,-10], [0,10], [10,10], [12,-10]]; -// path = hull2d_path(pts); -// move_copies(pts) color("red") sphere(1); -// polygon(points=pts, paths=[path]); -function hull2d_path(points) = - assert(is_path(points,2),"Invalid input to hull2d_path") - len(points) < 2 ? [] - : len(points) == 2 ? [0,1] - : let(tri=noncollinear_triple(points, error=false)) - tri == [] ? _hull_collinear(points) - : let( - remaining = [ for (i = [0:1:len(points)-1]) if (i != tri[0] && i!=tri[1] && i!=tri[2]) i ], - ccw = triangle_area(points[tri[0]], points[tri[1]], points[tri[2]]) > 0, - polygon = ccw ? [tri[0],tri[1],tri[2]] : [tri[0],tri[2],tri[1]] - ) _hull2d_iterative(points, polygon, remaining); - - - -// Adds the remaining points one by one to the convex hull -function _hull2d_iterative(points, polygon, remaining, _i=0) = - (_i >= len(remaining))? polygon : let ( - // pick a point - i = remaining[_i], - // find the segments that are in conflict with the point (point not inside) - conflicts = _find_conflicting_segments(points, polygon, points[i]) - // no conflicts, skip point and move on - ) (len(conflicts) == 0)? _hull2d_iterative(points, polygon, remaining, _i+1) : let( - // find the first conflicting segment and the first not conflicting - // conflict will be sorted, if not wrapping around, do it the easy way - polygon = _remove_conflicts_and_insert_point(polygon, conflicts, i) - ) _hull2d_iterative(points, polygon, remaining, _i+1); - - -function _hull_collinear(points) = - let( - a = points[0], - n = points[1] - a, - points1d = [ for(p = points) (p-a)*n ], - min_i = min_index(points1d), - max_i = max_index(points1d) - ) [min_i, max_i]; - - -function _find_conflicting_segments(points, polygon, point) = [ - for (i = [0:1:len(polygon)-1]) let( - j = (i+1) % len(polygon), - p1 = points[polygon[i]], - p2 = points[polygon[j]], - area = triangle_area(p1, p2, point) - ) if (area < 0) i -]; - - -// remove the conflicting segments from the polygon -function _remove_conflicts_and_insert_point(polygon, conflicts, point) = - (conflicts[0] == 0)? let( - nonconflicting = [ for(i = [0:1:len(polygon)-1]) if (!in_list(i, conflicts)) i ], - new_indices = concat(nonconflicting, (nonconflicting[len(nonconflicting)-1]+1) % len(polygon)), - polygon = concat([ for (i = new_indices) polygon[i] ], point) - ) polygon : let( - before_conflicts = [ for(i = [0:1:min(conflicts)]) polygon[i] ], - after_conflicts = (max(conflicts) >= (len(polygon)-1))? [] : [ for(i = [max(conflicts)+1:1:len(polygon)-1]) polygon[i] ], - polygon = concat(before_conflicts, point, after_conflicts) - ) polygon; - - - -// Function: hull3d_faces() -// Usage: -// hull3d_faces(points) -// Description: -// Takes a list of arbitrary 3D points, and finds the convex hull polyhedron to enclose -// them. Returns a list of triangular faces, where each face is a list of indexes into the given `points` -// list. The output will be valid for use with the polyhedron command, but may include vertices that are in the interior of a face of the hull, so it is not -// necessarily the minimal representation of the hull. -// If all points passed to it are coplanar, then the return is the list of indices of points -// forming the convex hull polygon. -// Example(3D): -// pts = [[-20,-20,0], [20,-20,0], [0,20,5], [0,0,20]]; -// faces = hull3d_faces(pts); -// move_copies(pts) color("red") sphere(1); -// %polyhedron(points=pts, faces=faces); -function hull3d_faces(points) = - assert(is_path(points,3),"Invalid input to hull3d_faces") - len(points) < 3 ? list_range(len(points)) - : let ( // start with a single non-collinear triangle - tri = noncollinear_triple(points, error=false) - ) - tri==[] ? _hull_collinear(points) - : let( - a = tri[0], - b = tri[1], - c = tri[2], - plane = plane3pt_indexed(points, a, b, c), - d = _find_first_noncoplanar(plane, points, 3) - ) - d == len(points) - ? /* all coplanar*/ - let ( - pts2d = [ for (p = points) project_plane(p, points[a], points[b], points[c]) ], - hull2d = hull2d_path(pts2d) - ) hull2d - : let( - remaining = [for (i = [0:1:len(points)-1]) if (i!=a && i!=b && i!=c && i!=d) i], - // Build an initial tetrahedron. - // Swap b, c if d is in front of triangle t. - ifop = in_front_of_plane(plane, points[d]), - bc = ifop? [c,b] : [b,c], - b = bc[0], - c = bc[1], - triangles = [ - [a,b,c], - [d,b,a], - [c,d,a], - [b,d,c] - ], - // calculate the plane equations - planes = [ for (t = triangles) plane3pt_indexed(points, t[0], t[1], t[2]) ] - ) _hull3d_iterative(points, triangles, planes, remaining); - - -// Adds the remaining points one by one to the convex hull -function _hull3d_iterative(points, triangles, planes, remaining, _i=0) = - _i >= len(remaining) ? triangles : - let ( - // pick a point - i = remaining[_i], - // find the triangles that are in conflict with the point (point not inside) - conflicts = _find_conflicts(points[i], planes), - // for all triangles that are in conflict, collect their halfedges - halfedges = [ - for(c = conflicts, i = [0:2]) let( - j = (i+1)%3 - ) [triangles[c][i], triangles[c][j]] - ], - // find the outer perimeter of the set of conflicting triangles - horizon = _remove_internal_edges(halfedges), - // generate a new triangle for each horizon halfedge together with the picked point i - new_triangles = [ for (h = horizon) concat(h,i) ], - // calculate the corresponding plane equations - new_planes = [ for (t = new_triangles) plane3pt_indexed(points, t[0], t[1], t[2]) ] - ) _hull3d_iterative( - points, - // remove the conflicting triangles and add the new ones - concat(list_remove(triangles, conflicts), new_triangles), - concat(list_remove(planes, conflicts), new_planes), - remaining, - _i+1 - ); - - -function _remove_internal_edges(halfedges) = [ - for (h = halfedges) - if (!in_list(reverse(h), halfedges)) - h -]; - - -function _find_conflicts(point, planes) = [ - for (i = [0:1:len(planes)-1]) - if (in_front_of_plane(planes[i], point)) - i -]; - - -function _find_first_noncoplanar(plane, points, i) = - (i >= len(points) || !points_on_plane([points[i]],plane))? i : - _find_first_noncoplanar(plane, points, i+1); - - -// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap +////////////////////////////////////////////////////////////////////// +// LibFile: hull.scad +// Functions to create 2D and 3D convex hulls. +// To use, add the following line to the beginning of your file: +// ``` +// include +// include +// ``` +// Derived from Oskar Linde's Hull: +// - https://github.com/openscad/scad-utils +////////////////////////////////////////////////////////////////////// + + +// Section: Convex Hulls + + +// Function: hull() +// Usage: +// hull(points); +// Description: +// Takes a list of 2D or 3D points (but not both in the same list) and returns either the list of +// indexes into `points` that forms the 2D convex hull perimeter path, or the list of faces that +// form the 3d convex hull surface. Each face is a list of indexes into `points`. If the input +// points are co-linear, the result will be the indexes of the two extrema points. If the input +// points are co-planar, the results will be a simple list of vertex indices that will form a planar +// perimeter. Otherwise a list of faces will be returned, where each face is a simple list of +// vertex indices for the perimeter of the face. +// Arguments: +// points = The set of 2D or 3D points to find the hull of. +function hull(points) = + assert(is_path(points),"Invalid input to hull") + len(points[0]) == 2 + ? hull2d_path(points) + : hull3d_faces(points); + + +// Module: hull_points() +// Usage: +// hull_points(points, [fast]); +// Description: +// If given a list of 2D points, creates a 2D convex hull polygon that encloses all those points. +// If given a list of 3D points, creates a 3D polyhedron that encloses all the points. This should +// handle about 4000 points in slow mode. If `fast` is set to true, this should be able to handle +// far more. +// Arguments: +// points = The list of points to form a hull around. +// fast = If true, uses a faster cheat that may handle more points, but also may emit warnings that can stop your script if you have "Halt on first warning" enabled. Default: false +// Example(2D): +// pts = [[-10,-10], [0,10], [10,10], [12,-10]]; +// hull_points(pts); +// Example: +// pts = [for (phi = [30:60:150], theta = [0:60:359]) spherical_to_xyz(10, theta, phi)]; +// hull_points(pts); +module hull_points(points, fast=false) { + if (points) { + assert(is_list(points[0])); + if (fast) { + if (len(points[0]) == 2) { + hull() polygon(points=points); + } else { + extra = len(points)%3; + faces = concat( + [[for(i=[0:1:extra+2])i]], + [for(i=[extra+3:3:len(points)-3])[i,i+1,i+2]] + ); + hull() polyhedron(points=points, faces=faces); + } + } else { + perim = hull(points); + if (is_num(perim[0])) { + polygon(points=points, paths=[perim]); + } else { + polyhedron(points=points, faces=perim); + } + } + } +} + + +// Function: hull2d_path() +// Usage: +// hull2d_path(points) +// Description: +// Takes a list of arbitrary 2D points, and finds the convex hull polygon to enclose them. +// Returns a path as a list of indices into `points`. May return extra points, that are on edges of the hull. +// Example(2D): +// pts = [[-10,-10], [0,10], [10,10], [12,-10]]; +// path = hull2d_path(pts); +// move_copies(pts) color("red") sphere(1); +// polygon(points=pts, paths=[path]); +function hull2d_path(points) = + assert(is_path(points,2),"Invalid input to hull2d_path") + len(points) < 2 ? [] + : len(points) == 2 ? [0,1] + : let(tri=noncollinear_triple(points, error=false)) + tri == [] ? _hull_collinear(points) + : let( + remaining = [ for (i = [0:1:len(points)-1]) if (i != tri[0] && i!=tri[1] && i!=tri[2]) i ], + ccw = triangle_area(points[tri[0]], points[tri[1]], points[tri[2]]) > 0, + polygon = ccw ? [tri[0],tri[1],tri[2]] : [tri[0],tri[2],tri[1]] + ) _hull2d_iterative(points, polygon, remaining); + + + +// Adds the remaining points one by one to the convex hull +function _hull2d_iterative(points, polygon, remaining, _i=0) = + (_i >= len(remaining))? polygon : let ( + // pick a point + i = remaining[_i], + // find the segments that are in conflict with the point (point not inside) + conflicts = _find_conflicting_segments(points, polygon, points[i]) + // no conflicts, skip point and move on + ) (len(conflicts) == 0)? _hull2d_iterative(points, polygon, remaining, _i+1) : let( + // find the first conflicting segment and the first not conflicting + // conflict will be sorted, if not wrapping around, do it the easy way + polygon = _remove_conflicts_and_insert_point(polygon, conflicts, i) + ) _hull2d_iterative(points, polygon, remaining, _i+1); + + +function _hull_collinear(points) = + let( + a = points[0], + n = points[1] - a, + points1d = [ for(p = points) (p-a)*n ], + min_i = min_index(points1d), + max_i = max_index(points1d) + ) [min_i, max_i]; + + +function _find_conflicting_segments(points, polygon, point) = [ + for (i = [0:1:len(polygon)-1]) let( + j = (i+1) % len(polygon), + p1 = points[polygon[i]], + p2 = points[polygon[j]], + area = triangle_area(p1, p2, point) + ) if (area < 0) i +]; + + +// remove the conflicting segments from the polygon +function _remove_conflicts_and_insert_point(polygon, conflicts, point) = + (conflicts[0] == 0)? let( + nonconflicting = [ for(i = [0:1:len(polygon)-1]) if (!in_list(i, conflicts)) i ], + new_indices = concat(nonconflicting, (nonconflicting[len(nonconflicting)-1]+1) % len(polygon)), + polygon = concat([ for (i = new_indices) polygon[i] ], point) + ) polygon : let( + before_conflicts = [ for(i = [0:1:min(conflicts)]) polygon[i] ], + after_conflicts = (max(conflicts) >= (len(polygon)-1))? [] : [ for(i = [max(conflicts)+1:1:len(polygon)-1]) polygon[i] ], + polygon = concat(before_conflicts, point, after_conflicts) + ) polygon; + + + +// Function: hull3d_faces() +// Usage: +// hull3d_faces(points) +// Description: +// Takes a list of arbitrary 3D points, and finds the convex hull polyhedron to enclose +// them. Returns a list of triangular faces, where each face is a list of indexes into the given `points` +// list. The output will be valid for use with the polyhedron command, but may include vertices that are in the interior of a face of the hull, so it is not +// necessarily the minimal representation of the hull. +// If all points passed to it are coplanar, then the return is the list of indices of points +// forming the convex hull polygon. +// Example(3D): +// pts = [[-20,-20,0], [20,-20,0], [0,20,5], [0,0,20]]; +// faces = hull3d_faces(pts); +// move_copies(pts) color("red") sphere(1); +// %polyhedron(points=pts, faces=faces); +function hull3d_faces(points) = + assert(is_path(points,3),"Invalid input to hull3d_faces") + len(points) < 3 ? list_range(len(points)) + : let ( // start with a single non-collinear triangle + tri = noncollinear_triple(points, error=false) + ) + tri==[] ? _hull_collinear(points) + : let( + a = tri[0], + b = tri[1], + c = tri[2], + plane = plane3pt_indexed(points, a, b, c), + d = _find_first_noncoplanar(plane, points, 3) + ) + d == len(points) + ? /* all coplanar*/ + let ( + pts2d = [ for (p = points) project_plane(p, points[a], points[b], points[c]) ], + hull2d = hull2d_path(pts2d) + ) hull2d + : let( + remaining = [for (i = [0:1:len(points)-1]) if (i!=a && i!=b && i!=c && i!=d) i], + // Build an initial tetrahedron. + // Swap b, c if d is in front of triangle t. + ifop = in_front_of_plane(plane, points[d]), + bc = ifop? [c,b] : [b,c], + b = bc[0], + c = bc[1], + triangles = [ + [a,b,c], + [d,b,a], + [c,d,a], + [b,d,c] + ], + // calculate the plane equations + planes = [ for (t = triangles) plane3pt_indexed(points, t[0], t[1], t[2]) ] + ) _hull3d_iterative(points, triangles, planes, remaining); + + +// Adds the remaining points one by one to the convex hull +function _hull3d_iterative(points, triangles, planes, remaining, _i=0) = + _i >= len(remaining) ? triangles : + let ( + // pick a point + i = remaining[_i], + // find the triangles that are in conflict with the point (point not inside) + conflicts = _find_conflicts(points[i], planes), + // for all triangles that are in conflict, collect their halfedges + halfedges = [ + for(c = conflicts, i = [0:2]) let( + j = (i+1)%3 + ) [triangles[c][i], triangles[c][j]] + ], + // find the outer perimeter of the set of conflicting triangles + horizon = _remove_internal_edges(halfedges), + // generate a new triangle for each horizon halfedge together with the picked point i + new_triangles = [ for (h = horizon) concat(h,i) ], + // calculate the corresponding plane equations + new_planes = [ for (t = new_triangles) plane3pt_indexed(points, t[0], t[1], t[2]) ] + ) _hull3d_iterative( + points, + // remove the conflicting triangles and add the new ones + concat(list_remove(triangles, conflicts), new_triangles), + concat(list_remove(planes, conflicts), new_planes), + remaining, + _i+1 + ); + + +function _remove_internal_edges(halfedges) = [ + for (h = halfedges) + if (!in_list(reverse(h), halfedges)) + h +]; + + +function _find_conflicts(point, planes) = [ + for (i = [0:1:len(planes)-1]) + if (in_front_of_plane(planes[i], point)) + i +]; + + +function _find_first_noncoplanar(plane, points, i) = + (i >= len(points) || !points_on_plane([points[i]],plane))? i : + _find_first_noncoplanar(plane, points, i+1); + + +// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/vnf.scad b/vnf.scad index 822328f..ad26b90 100644 --- a/vnf.scad +++ b/vnf.scad @@ -357,6 +357,33 @@ module vnf_polyhedron(vnf, convexity=2, extent=true, cp=[0,0,0], anchor="origin" +// Module: vnf_wireframe() +// Usage: +// vnf_wireframe(vnf, [r|d]); +// Description: +// Given a VNF, creates a wire frame ball-and-stick model of the polyhedron with a cylinder for each edge and a sphere at each vertex. +// Arguments: +// vnf = A vnf structure +// r|d = radius or diameter of the cylinders forming the wire frame. Default: r=1 +// Example: +// $fn=32; +// ball = sphere(r=20, $fn=6); +// vnf_wireframe(ball,d=1); +// Example: +// include +// $fn=32; +// cube_oct = regular_polyhedron_info("vnf", name="cuboctahedron", or=20); +// vnf_wireframe(cube_oct); +module vnf_wireframe(vnf, r, d) +{ + r = get_radius(r=r,d=d,dflt=1); + vertex = vnf[0]; + edges = unique([for (face=vnf[1], i=idx(face)) + sort([face[i], select(face,i+1)]) + ]); + for (e=edges) extrude_from_to(vertex[e[0]],vertex[e[1]]) circle(r=r); + move_copies(vertex) sphere(r=r); +} // Function: vnf_volume()