From 452f31727ec762776cd8bfcc570848fe0cd838e2 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Thu, 16 Jan 2025 23:34:47 +0900 Subject: [PATCH 1/7] chore: update comparisons.scad occurences -> occurrences --- comparisons.scad | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/comparisons.scad b/comparisons.scad index 1749195a..972f6de5 100644 --- a/comparisons.scad +++ b/comparisons.scad @@ -356,7 +356,7 @@ function compare_lists(a, b) = // If `all` is true then returns a list of all indices where the minimum value occurs. // Arguments: // vals = vector of values -// all = set to true to return indices of all occurences of the minimum. Default: false +// all = set to true to return indices of all occurrences of the minimum. Default: false // Example: // a = min_index([5,3,9,6,2,7,8,2,1]); // Returns: 8 // b = min_index([5,3,9,6,2,7,8,2,7],all=true); // Returns: [4,7] @@ -377,7 +377,7 @@ function min_index(vals, all=false) = // If `all` is true then returns a list of all indices where the maximum value occurs. // Arguments: // vals = vector of values -// all = set to true to return indices of all occurences of the maximum. Default: false +// all = set to true to return indices of all occurrences of the maximum. Default: false // Example: // max_index([5,3,9,6,2,7,8,9,1]); // Returns: 2 // max_index([5,3,9,6,2,7,8,9,1],all=true); // Returns: [2,7] From 5e8547db6c78afdb14eef018f91b9550ad26065d Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Fri, 17 Jan 2025 17:27:36 -0800 Subject: [PATCH 2/7] Doc improvements: spelling, grammar, examples --- comparisons.scad | 40 +++++++++---------- vnf.scad | 102 +++++++++++++++++++++++------------------------ 2 files changed, 71 insertions(+), 71 deletions(-) diff --git a/comparisons.scad b/comparisons.scad index 1749195a..83d1af15 100644 --- a/comparisons.scad +++ b/comparisons.scad @@ -23,7 +23,7 @@ // Arguments: // a = First value. // b = Second value. -// eps = The maximum allowed difference between `a` and `b` that will return true. Defaults to 1e-9. +// eps = The maximum allowed difference between `a` and `b` to consider as "no difference". Default: 1e-9. // Example: // test1 = approx(-0.3333333333,-1/3); // Returns: true // test2 = approx(0.3333333333,1/3); // Returns: true @@ -47,7 +47,7 @@ function approx(a,b,eps=EPSILON) = // Function: all_zero() -// Synopsis: Returns true if the value(s) given are aproximately zero. +// Synopsis: Returns true if the value(s) given are approximately zero. // Topics: Comparisons, List Handling // See Also: approx(), all_zero(), all_nonzero() // Usage: @@ -70,7 +70,7 @@ function all_zero(x, eps=EPSILON) = // Function: all_nonzero() -// Synopsis: Returns true if the value(s) given are not aproximately zero. +// Synopsis: Returns true if the value(s) given are not approximately zero. // Topics: Comparisons, List Handling // See Also: approx(), all_zero(), all_nonzero() // Usage: @@ -242,7 +242,7 @@ function are_ends_equal(list, eps=EPSILON) = // bool = is_increasing(list, [strict]); // Description: // Returns true if the list is (non-strictly) increasing, or strictly increasing if `strict=true`. -// The list can be a list of any items that OpenSCAD can compare, or it can be a string which will be +// The list can be a list of any items that OpenSCAD can compare, or it can be a string, which gets // evaluated character by character. // Arguments: // list = list (or string) to check @@ -267,7 +267,7 @@ function is_increasing(list,strict=false) = // bool = is_decreasing(list, [strict]); // Description: // Returns true if the list is (non-strictly) decreasing, or strictly decreasing if `strict=true`. -// The list can be a list of any items that OpenSCAD can compare, or it can be a string which will be +// The list can be a list of any items that OpenSCAD can compare, or it can be a string, which gets // evaluated character by character. // Arguments: // list = list (or string) to check @@ -345,7 +345,7 @@ function compare_lists(a, b) = // Function: min_index() -// Synopsis: Returns the index of the minimal value in the given list. +// Synopsis: Returns the index of the minimum value in the given list. // Topics: List Handling // See Also: max_index(), is_increasing(), is_decreasing() // Usage: @@ -356,7 +356,7 @@ function compare_lists(a, b) = // If `all` is true then returns a list of all indices where the minimum value occurs. // Arguments: // vals = vector of values -// all = set to true to return indices of all occurences of the minimum. Default: false +// all = set to true to return indices of all occurrences of the minimum. Default: false // Example: // a = min_index([5,3,9,6,2,7,8,2,1]); // Returns: 8 // b = min_index([5,3,9,6,2,7,8,2,7],all=true); // Returns: [4,7] @@ -366,7 +366,7 @@ function min_index(vals, all=false) = // Function: max_index() -// Synopsis: Returns the index of the minimal value in the given list. +// Synopsis: Returns the index of the maximum value in the given list. // Topics: List Handling // See Also: min_index(), is_increasing(), is_decreasing() // Usage: @@ -377,7 +377,7 @@ function min_index(vals, all=false) = // If `all` is true then returns a list of all indices where the maximum value occurs. // Arguments: // vals = vector of values -// all = set to true to return indices of all occurences of the maximum. Default: false +// all = set to true to return indices of all occurrences of the maximum. Default: false // Example: // max_index([5,3,9,6,2,7,8,9,1]); // Returns: 2 // max_index([5,3,9,6,2,7,8,9,1],all=true); // Returns: [2,7] @@ -390,7 +390,7 @@ function max_index(vals, all=false) = // Function: find_approx() -// Synopsis: Finds the indexes of the item(s) in the given list that are aproximately the given value. +// Synopsis: Finds the indexes of the item(s) in the given list that are approximately the given value. // Topics: List Handling // See Also: in_list() // Usage: @@ -763,7 +763,7 @@ function _indexed_sort(arrind) = // Usage: // slist = sort(list, [idx]); // Description: -// Sorts the given list in lexicographic order. The sort is stable, meaning equivalent items will not change order. +// Sorts the given list in lexicographic order. The sort is stable, meaning equivalent items do not change order. // If the input is a homogeneous simple list or a homogeneous // list of vectors (see function is_homogeneous), the sorting method uses the native comparison operator and is faster. // When sorting non homogeneous list the elements are compared with `compare_vals`, with types ordered according to @@ -808,9 +808,9 @@ function sort(list, idx=undef) = // Description: // Given a list, sort it as function `sort()`, and returns // a list of indexes into the original list in that sorted order. -// The sort is stable, so equivalent items will not change order. +// The sort is stable, so equivalent items so not change order. // If you iterate the returned list in order, and use the list items -// to index into the original list, you will be iterating the original +// to index into the original list, then you are accessing the original // values in sorted order. // Arguments: // list = The list to sort. @@ -862,8 +862,8 @@ function sortidx(list, idx=undef) = // ulist = group_sort(list,[idx]); // Description: // Given a list of numbers, sorts the list into a sequence of lists, where each list contains any repeated values. -// If there are no repeated values the output will be a list of singleton lists. -// If you apply {{flatten()}} to the output, the result will be a simple sorted list. +// If there are no repeated values, the output is a list of singleton lists. +// If you apply {{flatten()}} to the output, the result is a simple sorted list. // . // When the input is a list of lists, the sorting is done based on index `idx` of the entries in `list`. // In this case, `list[i][idx]` must be a number for every `i`, and the entries in `list` are grouped @@ -897,9 +897,9 @@ function group_sort(list, idx) = // Description: // Given a list of integer group numbers, and an equal-length list of values, // returns a list of groups with the values sorted into the corresponding groups. -// Ie: if you have a groups index list of `[2,3,2]` and values of `["A","B","C"]`, then -// the values `"A"` and `"C"` will be put in group 2, and `"B"` will be in group 3. -// Groups that have no values grouped into them will be an empty list. So the +// For example: if you have a groups index list of `[2,3,2]` and values of `["A","B","C"]`, then +// the values `"A"` and `"C"` are put in group 2, and `"B"` is in group 3. +// Groups that have no values grouped into them are empty lists. Therefore, the // above would return `[[], [], ["A","C"], ["B"]]` // Arguments: // groups = A list of integer group index numbers. @@ -936,8 +936,8 @@ function group_data(groups, values) = // small = list_smallest(list, k) // Description: // Returns a set of the k smallest items in list in arbitrary order. The items must be -// mutually comparable with native OpenSCAD comparison operations. You will get "undefined operation" -// errors if you provide invalid input. +// mutually comparable with native OpenSCAD comparison operations. +// You get "undefined operation" errors if you provide invalid input. // Arguments: // list = list to process // k = number of items to return diff --git a/vnf.scad b/vnf.scad index 6ed86fa8..22edc63a 100644 --- a/vnf.scad +++ b/vnf.scad @@ -40,13 +40,13 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces. // back to the first column, or wrap the last row to the first. Endcaps can be added to either // the first and/or last rows. The style parameter determines how the quadrilaterals are divided into // triangles. The default style is an arbitrary, systematic subdivision in the same direction. The "alt" style -// is the uniform subdivision in the other (alternate) direction. The "flip1" style is an arbitrary division which alternates the +// is the uniform subdivision in the other (alternate) direction. The "flip1" style is an arbitrary division that alternates the // direction for any adjacent pair of quadrilaterals. The "flip2" style is the alternating division that is the opposite of "flip1". // The "min_edge" style picks the shorter edge to // subdivide for each quadrilateral, so the division may not be uniform across the shape. The "quincunx" style // adds a vertex in the center of each quadrilateral and creates four triangles, and the "convex" and "concave" styles // choose the locally convex/concave subdivision. The "min_area" option creates the triangulation with the minimal area. Degenerate faces -// are not included in the output, but if this results in unused vertices they will still appear in the output. +// are not included in the output, but if this results in unused vertices they still appear in the output. // Arguments: // points = A list of vertices to divide into columns and rows. // --- @@ -445,14 +445,14 @@ function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], // Description: // Given a list of VNF structures, merges them all into a single VNF structure. // Combines all the points of the input VNFs and labels the faces appropriately. -// All the points in the input VNFs will appear in the output, even if they are -// duplicates of each other. It is valid to repeat points in a VNF, but if you -// with to remove the duplicates that will occur along joined edges, use {{vnf_merge_points()}}. +// All the points in the input VNFs appear in the output, even if they are +// duplicated. It is valid to repeat points in a VNF, but if you +// with to remove the duplicates that occur along joined edges, use {{vnf_merge_points()}}. // . -// Note that this is a tool for manipulating polyhedron data. It is for +// Note that this is a tool for manipulating polyhedron data. It is for // building up a full polyhedron from partial polyhedra. -// It is *not* a union operator for VNFs. The VNFs to be joined must not intersect each other, -// except at edges, or the result will be an invalid polyhedron. Similarly the +// It is *not* a union operator for VNFs. The VNFs to be joined must not intersect each other, +// except at edges, otherwise the result is an invalid polyhedron. Also, the // result must not have any other illegal polyhedron characteristics, such as creating // more than two faces sharing the same edge. // If you want a valid result it is your responsibility to ensure that the polyhedron @@ -492,7 +492,7 @@ function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[], // for(theta=[0:90:359]) zrot(theta,top) // ]); // vnf_polyhedron(full); -// Example(3D): The vnf_join function is not a union operator for polyhedra. If any faces intersect, like they do in this example where we combine the faces of two cubes, the result is invalid and will give rise to CGAL errors when you add more objects into the model. +// Example(3D): The vnf_join function is not a union operator for polyhedra. If any faces intersect, like they do in this example where we combine the faces of two cubes, the result is invalid and results in CGAL errors when you add more objects into the model. // cube1 = cube(5); // cube2 = move([2,2,2],cube1); // badvnf = vnf_join([cube1,cube2]); @@ -532,7 +532,7 @@ function vnf_join(vnfs) = // Description: // Given a list of 3D polygons, produces a VNF containing those polygons. // It is up to the caller to make sure that the points are in the correct order to make the face -// normals point outwards. No checking for duplicate vertices is done. If you want to +// normals point outward. No checking for duplicate vertices is done. If you want to // remove duplicate vertices use {{vnf_merge_points()}}. Polygons with zero area are discarded from the face list by default. // If you give non-coplanar faces an error is displayed. These checks increase run time by about 2x for triangular polygons, but // about 10x for pentagons; the checks can be disabled by setting fast=true. @@ -725,9 +725,9 @@ function _bridge(pt, outer,eps) = // Given a (two-dimensional) region, applies the given transformation matrix to it and makes a (three-dimensional) triangulated VNF of // faces for that region, reversed if desired. // Arguments: -// region = The region to convert to a vnf. -// transform = If given, a transformation matrix to apply to the faces generated from the region. Default: No transformation applied. -// reverse = If true, reverse the normals of the faces generated from the region. An untransformed region will have face normals pointing `UP`. Default: false +// region = The region to convert to a VNF. +// transform = If given, a transformation matrix to apply to the faces generated from the region. Default: No transformation applied. +// reverse = If true, reverse the normals of the faces generated from the region. An untransformed region has face normals pointing `UP`. Default: false // Example(3D): // region = [square([20,10],center=true), // right(5,square(4,center=true)), @@ -948,8 +948,8 @@ function vnf_triangulate(vnf) = // want to be able to identify the true faces. This function merges together the triangles that // form those true faces, turning a VNF where each true face is represented by a single entry // in the faces list of the VNF. This function requires that the true faces have no internal vertices. -// This will always be true for a triangulated VNF, but might fail for a VNF with some other -// face partition. If internal vertices are present, the output will include backtracking paths from +// This is always true for a triangulated VNF, but might fail for a VNF with some other +// face partition. If internal vertices are present, the output includes backtracking paths from // the boundary to all of those vertices. // Arguments: // vnf = vnf whose faces you want to unify @@ -1176,7 +1176,7 @@ function _slice_3dpolygons(polys, dir, cuts) = // cp = Centerpoint for determining intersection anchors or centering the shape. Determines the base of the anchor vector. Can be "centroid", "mean", "box" or a 3D point. Default: "centroid" // anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"` // spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0` -// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` +// orient = Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP` // atype = Select "hull" or "intersect" anchor type. Default: "hull" // Anchor Types: // "hull" = Anchors to the virtual convex hull of the shape. @@ -1205,19 +1205,19 @@ module vnf_polyhedron(vnf, convexity=2, cp="centroid", anchor="origin", spin=0, // each edge and a sphere at each vertex. The width parameter specifies the width of the sticks // that form the wire frame and the diameter of the balls. // Arguments: -// vnf = A vnf structure +// vnf = A VNF structure // width = width of the cylinders forming the wire frame. Default: 1 -// Example: +// Example(3D): // $fn=32; // ball = sphere(r=20, $fn=6); // vnf_wireframe(ball,width=1); -// Example: +// Example(3D): // include // $fn=32; // cube_oct = regular_polyhedron_info("vnf", // name="cuboctahedron", or=20); // vnf_wireframe(cube_oct); -// Example: The spheres at the vertex are imperfect at aligning with the cylinders, so especially at low $fn things look prety ugly. This is normal. +// Example(3D): The spheres at the vertex are imperfect at aligning with the cylinders, so especially at low $fn things look prety ugly. This is normal. // include // $fn=8; // octahedron = regular_polyhedron_info("vnf", @@ -1386,8 +1386,8 @@ function projection(vnf,cut=false,eps=EPSILON) = // then closed=true is may produce invalid results when it tries to construct closing faces // on the cut plane. Set closed=false for such inputs. // . -// If you set boundary to true then the return will be the pair [vnf,boundary] where vnf is the -// vnf as usual (with closed=false) and boundary is a list giving each connected component of the cut +// If you set `boundary=true` then the return is the pair `[vnf,boundary]`, where `vnf` is the +// VNF as usual (with `closed=false`) and boundary is a list giving each connected component of the cut // boundary surface. Each entry in boundary is a list of index values that index into the vnf vertex list (vnf[0]). // This makes it possible to construct mating shapes, e.g. with {{skin()}} or {{vnf_vertex_array()}} that // can be combined using {{vnf_join()}} to make a valid polyhedron. @@ -1426,22 +1426,22 @@ function projection(vnf,cut=false,eps=EPSILON) = // knot=path_sweep(ushape, knot_path, closed=true, method="incremental"); // cut_knot = vnf_halfspace([1,0,0,0], knot); // vnf_polyhedron(cut_knot); -// Example(VPR=[80,0,15]): Cut a sphere with an arbitrary plane +// Example(3D,VPR=[80,0,15]): Cut a sphere with an arbitrary plane // vnf1=sphere(r=50, style="icosa", $fn=16); // vnf2=vnf_halfspace([.8,1,-1.5,0], vnf1); // vnf_polyhedron(vnf2); -// Example(VPR=[80,0,15]): Cut it again, but with closed=false to leave an open boundary. +// Example(3D,VPR=[80,0,15]): Cut it again, but with closed=false to leave an open boundary. // vnf1=sphere(r=50, style="icosa", $fn=16); // vnf2=vnf_halfspace([.8,1,-1.5,0], vnf1); // vnf3=vnf_halfspace([0,0,-1,0], vnf2, closed=false); // vnf_polyhedron(vnf3); -// Example(VPR=[80,0,15]): Use {vnf_join()} to combine with a mating vnf, in this case a reflection of the part we made. +// Example(3D,VPR=[80,0,15]): Use {vnf_join()} to combine with a mating vnf, in this case a reflection of the part we made. // vnf1=sphere(r=50, style="icosa", $fn=16); // vnf2=vnf_halfspace([.8,1,-1.5,0], vnf1); // vnf3=vnf_halfspace([0,0,-1,0], vnf2, closed=false); // vnf4=vnf_join([vnf3, zflip(vnf3,1)]); // vnf_polyhedron(vnf4); -// Example: When the input VNF is a surface with a boundary, if you use the default setting closed=true, then vnf_halfspace() tries to construct closing faces from the edges created by the cut. These faces may be invalid, for example if the cut points are collinear. In this example the constructed face is a valid face. +// Example(3D): When the input VNF is a surface with a boundary, if you use the default setting closed=true, then vnf_halfspace() tries to construct closing faces from the edges created by the cut. These faces may be invalid, for example if the cut points are collinear. In this example the constructed face is a valid face. // patch=[ // [[10,-10,0],[1,-1,0],[-1,-1,0],[-10,-10,0]], // [[10,-10,20],[1,-1,20],[-1,-1,20],[-10,-10,20]] @@ -1449,7 +1449,7 @@ function projection(vnf,cut=false,eps=EPSILON) = // vnf=bezier_vnf(patch); // vnfcut = vnf_halfspace([-.8,0,-1,-14],vnf); // vnf_polyhedron(vnfcut); -// Example: Setting closed to false eliminates this (possibly invalid) face: +// Example(3D): Setting closed to false eliminates this (possibly invalid) face: // patch=[ // [[10,-10,0],[1,-1,0],[-1,-1,0],[-10,-10,0]], // [[10,-10,20],[1,-1,20],[-1,-1,20],[-10,-10,20]] @@ -1457,18 +1457,18 @@ function projection(vnf,cut=false,eps=EPSILON) = // vnf=bezier_vnf(patch); // vnfcut = vnf_halfspace([-.8,0,-1,-14],vnf,closed=false); // vnf_polyhedron(vnfcut); -// Example: Here is a VNF that has holes, so it is not a valid manifold. +// Example(3D): Here is a VNF that has holes, so it is not a valid manifold. // outside = linear_sweep(circle(r=30), h=100, caps=false); // inside = yrot(7,linear_sweep(circle(r=10), h=120, caps=false)); // open_vnf=vnf_join([outside, vnf_reverse_faces(inside)]); // vnf_polyhedron(open_vnf); -// Example: By cutting it at each end we can create closing faces, resulting in a valid manifold without holes. +// Example(3D): By cutting it at each end we can create closing faces, resulting in a valid manifold without holes. // outside = linear_sweep(circle(r=30), h=100, caps=false); // inside = yrot(11,linear_sweep(circle(r=10), h=120, caps=false)); // open_vnf=vnf_join([outside, vnf_reverse_faces(inside)]); // vnf = vnf_halfspace([0,0,1,5], vnf_halfspace([0,.7,-1,-75], open_vnf)); // vnf_polyhedron(vnf); -// Example: If boundary=true then the return is a list with the VNF and boundary data. +// Example(3D): If boundary=true then the return is a list with the VNF and boundary data. // vnf = path_sweep(circle(r=4, $fn=16), // circle(r=20, $fn=64),closed=true); // cut_bnd = vnf_halfspace([-1,1,-4,0], vnf, boundary=true);*/ @@ -1586,8 +1586,8 @@ function _triangulate_planar_convex_polygons(polys) = // it may intersect itself, which produces an invalid polyhedron. It is your responsibility to // avoid this situation. The 1:1 // radius is where the curved length of the bent VNF matches the length of the original VNF. If the -// `r` or `d` arguments are given, then they will specify the 1:1 radius or diameter. If they are -// not given, then the 1:1 radius will be defined by the distance of the furthest vertex in the +// `r` or `d` arguments are given, then they specify the 1:1 radius or diameter. If they are +// not given, then the 1:1 radius is defined by the distance of the furthest vertex in the // original VNF from the Z=0 plane. You can adjust the granularity of the bend using the standard // `$fa`, `$fs`, and `$fn` variables. // Arguments: @@ -1695,9 +1695,9 @@ function vnf_bend(vnf,r,d,axis="Z") = // hull_vnf(vnf,[fast]); // Description: // Given a VNF or a list of 3d points, compute the convex hull -// and return it as a VNF. This differs from {{hull()}} and {{hull3d_faces()}} which -// return just the face list referenced to the input point list. Note that the point -// list that is returned will contain all the points that are actually used in the input +// and return it as a VNF. This differs from {{hull()}} and {{hull3d_faces()}}, which +// return just the face list referenced to the input point list. Note that the returned +// point list contains all the points that are actually used in the input // VNF, which may be many more points than are needed to represent the convex hull. // This is not usually a problem, but you can run the somewhat slow {{vnf_drop_unused_points()}} // function to fix this if necessary. @@ -1747,7 +1747,7 @@ function _sort_pairs0(arr) = // Function: vnf_boundary() -// Synopsis: Returns the boundary of a VNF as an list of paths +// Synopsis: Returns the boundary of a VNF as a list of paths // SynTags: VNF // Topics: VNF Manipulation // See Also: vnf_halfspace(), vnf_merge_points() @@ -1759,13 +1759,13 @@ function _sort_pairs0(arr) = // set `merge=false` to disable the automatic point merge and save time. The result of running on a VNF with duplicate points is likely to // be incorrect or invalid; it may produce obscure errors. // . -// The output will be a list of closed 3D paths. If the VNF has no boundary then the output is `[]`. The boundary path(s) are +// The output is a list of closed 3D paths. If the VNF has no boundary then the output is `[]`. The boundary path(s) are // traversed in the same direction as the edges in the original VNF. // . // It is sometimes desirable to have the boundary available as an index list into the VNF vertex list. However, merging the points in the VNF changes the // VNF vertex point list. If you set `merge=false` you can also set `idx=true` to get an index list. As noted above, you must be certain // that your in put VNF has no duplicate vertices, perhaps by running {{vnf_merge_points()}} yourself on it. With `idx=true` -// the output will be indices into the VNF vertex list, which enables you to associate the vertices on the boundary path with the original VNF. +// the output consists of indices into the VNF vertex list, which enables you to associate the vertices on the boundary path with the original VNF. // Arguments: // vnf = input vnf // --- @@ -1822,10 +1822,10 @@ function vnf_boundary(vnf,merge=true,idx=false) = // Computes a simple offset of a VNF by estimating the normal at every point based on the weighted average of surrounding polygons // in the mesh. The offset distance, `delta`, must be small enough so that no self-intersection occurs, which is no issue when the // curvature is positive (like the outside of a sphere) but for negative curvature it means the offset distance must be smaller -// than the smallest radius of curvature of the VNF. If self-intersection -// occurs, the resulting geometry will be invalid and you will get an error when you introduce a second object into the model. +// than the smallest radius of curvature of the VNF. Any self-intersection that occurs +// invalidates the resulting geometry, giving you an error when you introduce a second object into the model. // **It is your responsibility to avoid invalid geometry!** It cannot be detected automatically. -// The positive offset direction is towards the outside of the VNF, the faces that are colored yellow in the "thrown together" view. +// The positive offset direction is toward the outside of the VNF, the faces that are colored yellow in the "thrown together" view. // . // **The input VNF must not contain duplicate points.** By default, vnf_small_offset() calls {{vnf_merge_points()}} // to remove duplicate points. Note, however, that this operation can be slow. If you are **certain** there are no duplicate points you can @@ -1887,14 +1887,14 @@ function vnf_small_offset(vnf, delta, merge=true) = // Constructs a thin sheet from a vnf by offsetting the vnf along the normal vectors estimated at // each vertex by averaging the normals of the adjacent faces. This is done using {{vnf_small_offset()}. // The thickness value must be small enough so that no points cross each other -// when the offset is computed, because that results in invalid geometry and will give rendering errors. +// when the offset is computed, because that results in invalid geometry and rendering errors. // Rendering errors may not manifest until you add other objects to your model. // **It is your responsibility to avoid invalid geometry!** // . // Once the offset to the original VNF is computed the original and offset VNF are connected by filling // in the boundary strip(s) between them // . -// When thickness is positive, the given bezier patch is extended towards its "inside", which is the +// When thickness is positive, the given bezier patch is extended toward its "inside", which is the // side that appears purple in the "thrown together" view. Note that this is the opposite direction // of {{vnf_small_offset()}}. Extending toward the inside means that your original VNF remains unchanged // in the output. You can extend the patch in the other direction @@ -1984,8 +1984,8 @@ module _show_vertices(vertices, size=1, filter) { /// _show_faces(vertices, faces, [size=], [filter=]); /// Description: /// Draws all the vertices at their 3D position, numbered in blue by their -/// position in the vertex array. Each face will have their face number drawn -/// in red, aligned with the center of face. All children of this module are drawn +/// position in the vertex array. Each face has its face number drawn +/// in red, aligned with the center of the face. All children of this module are drawn /// with transparency. /// Arguments: /// vertices = Array of point vertices. @@ -2039,8 +2039,8 @@ module _show_faces(vertices, faces, size=1, filter) { // Description: // A drop-in module to replace `vnf_polyhedron()` to help debug vertices and faces. // Draws all the vertices at their 3D position, numbered in blue by their -// position in the vertex array. Each face will have its face number drawn -// in red, aligned with the center of face. All given faces are drawn with +// position in the vertex array. Each face has its face number drawn +// in red, aligned with the center of the face. All given faces are drawn with // transparency. All children of this module are drawn with transparency. // Works best with Thrown-Together preview mode, to see reversed faces. // You can set opacity to 0 if you want to supress the display of the polyhedron faces. @@ -2056,7 +2056,7 @@ module _show_faces(vertices, faces, size=1, filter) { // opacity = Opacity of the polyhedron faces. Default: 0.5 // convexity = The max number of walls a ray can pass through the given polygon paths. // size = The size of the text used to label the faces and vertices. Default: 1 -// filter = If given a function literal of signature `function(i)`, will only show labels for vertices and faces that have a vertex index that gets a true result from that function. Default: no filter. +// filter = If given a function literal of signature `function(i)`, shows only labels for vertices and faces that have a vertex index that gets a true result from that function. Default: no filter. // Example(EdgesMed): // verts = [for (z=[-10,10], a=[0:120:359.9]) [10*cos(a),10*sin(a),z]]; // faces = [[0,1,2], [5,4,3], [0,3,4], [0,4,1], [1,4,5], [1,5,2], [2,5,3], [2,3,0]]; @@ -2111,8 +2111,8 @@ module debug_vnf(vnf, faces=true, vertices=true, opacity=0.5, size=1, convexity= // label_verts = If true, shows labels at each vertex that show the vertex number. Default: false // label_faces = If true, shows labels at the center of each face that show the face number. Default: false // wireframe = If true, shows edges more clearly so you can see them in Thrown Together mode. Default: false -// adjacent = If true, only display faces adjacent to a vertex listed in the errors. Default: false -// Example(3D,Edges): BIG_FACE Warnings; Faces with More Than 3 Vertices. CGAL often will fail to accept that a face is planar after a rotation, if it has more than 3 vertices. +// adjacent = If true, display only faces that are adjacent to a vertex listed in the errors. Default: false +// Example(3D,Edges): BIG_FACE Warnings; Faces with More Than 3 Vertices. CGAL often fails to accept that a face is planar after a rotation, if it has more than 3 vertices. // vnf = skin([ // path3d(regular_ngon(n=3, d=100),0), // path3d(regular_ngon(n=5, d=100),100) @@ -2251,7 +2251,7 @@ function _vnf_validate(vnf, show_warns=true, check_isects=false) = for(i = idx(dfaces), j = idx(dfaces)) if(i != j) for(edge1 = pair(faces[i],true)) for(edge2 = pair(faces[j],true)) - if(edge1 == edge2) // Valid adjacent faces will never have the same vertex ordering. + if(edge1 == edge2) // Valid adjacent faces must never have the same vertex ordering. if(_edge_not_reported(edge1, varr, multconn_edges)) _vnf_validate_err("REVERSAL", edge1) ]), From 4d699551f05c783ab4062758d6ebbe992201b14e Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Fri, 17 Jan 2025 21:19:38 -0800 Subject: [PATCH 3/7] Fixed syntax errors in examples after enabling rendering --- vnf.scad | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vnf.scad b/vnf.scad index 22edc63a..9812cad8 100644 --- a/vnf.scad +++ b/vnf.scad @@ -1427,16 +1427,16 @@ function projection(vnf,cut=false,eps=EPSILON) = // cut_knot = vnf_halfspace([1,0,0,0], knot); // vnf_polyhedron(cut_knot); // Example(3D,VPR=[80,0,15]): Cut a sphere with an arbitrary plane -// vnf1=sphere(r=50, style="icosa", $fn=16); +// vnf1=spheroid(r=50, style="icosa", $fn=16); // vnf2=vnf_halfspace([.8,1,-1.5,0], vnf1); // vnf_polyhedron(vnf2); // Example(3D,VPR=[80,0,15]): Cut it again, but with closed=false to leave an open boundary. -// vnf1=sphere(r=50, style="icosa", $fn=16); +// vnf1=spheroid(r=50, style="icosa", $fn=16); // vnf2=vnf_halfspace([.8,1,-1.5,0], vnf1); // vnf3=vnf_halfspace([0,0,-1,0], vnf2, closed=false); // vnf_polyhedron(vnf3); // Example(3D,VPR=[80,0,15]): Use {vnf_join()} to combine with a mating vnf, in this case a reflection of the part we made. -// vnf1=sphere(r=50, style="icosa", $fn=16); +// vnf1=spheroid(r=50, style="icosa", $fn=16); // vnf2=vnf_halfspace([.8,1,-1.5,0], vnf1); // vnf3=vnf_halfspace([0,0,-1,0], vnf2, closed=false); // vnf4=vnf_join([vnf3, zflip(vnf3,1)]); @@ -1471,7 +1471,7 @@ function projection(vnf,cut=false,eps=EPSILON) = // Example(3D): If boundary=true then the return is a list with the VNF and boundary data. // vnf = path_sweep(circle(r=4, $fn=16), // circle(r=20, $fn=64),closed=true); -// cut_bnd = vnf_halfspace([-1,1,-4,0], vnf, boundary=true);*/ +// cut_bnd = vnf_halfspace([-1,1,-4,0], vnf, boundary=true); // cutvnf = cut_bnd[0]; // boundary = [for(b=cut_bnd[1]) select(cutvnf[0],b)]; // vnf_polyhedron(cutvnf); From 2e62f07bf7fc4b1ecee354959daac7e01e2fcbcc Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Wed, 22 Jan 2025 22:43:08 -0800 Subject: [PATCH 4/7] Isosurfaces and metaballs, new features --- .openscad_docsgen_rc | 1 + isosurface.scad | 1404 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1405 insertions(+) create mode 100644 isosurface.scad diff --git a/.openscad_docsgen_rc b/.openscad_docsgen_rc index 0260ae3e..2551ae0b 100644 --- a/.openscad_docsgen_rc +++ b/.openscad_docsgen_rc @@ -69,6 +69,7 @@ PrioritizeFiles: tripod_mounts.scad walls.scad wiring.scad + isosurface.scad DefineHeader(BulletList): Side Effects DefineHeader(Table;Headers=Anchor Name|Position): Named Anchors DefineHeader(Table;Headers=Anchor Type|What it is): Anchor Types diff --git a/isosurface.scad b/isosurface.scad new file mode 100644 index 00000000..57b589f2 --- /dev/null +++ b/isosurface.scad @@ -0,0 +1,1404 @@ +///////////////////////////////////////////////////////////////////// +// LibFile: isosurface.scad +// An isosurface is a three-dimensional surface representing points of a constant +// value (e.g. density pressure, temperature, electric field strength, density) in a +// 3D volume. It is essentially a 3D cross-section of a 4-dimensional function. +// An isosurface may be represented generally by any function of three variables, +// that is, the function returns a single value based on [x,y,z] inputs. The +// isosurface is defined by all return values equal to a constant isovalue. +// . +// A [gryoid](https://en.wikipedia.org/wiki/Gyroid) (often used as a volume infill pattern in [FDM 3D printing](https://en.wikipedia.org/wiki/Fused_filament_fabrication)) +// is an exmaple of an isosurface that is unbounded and periodic in all three dimensions. +// Other typical examples in 3D graphics are [metaballs](https://en.wikipedia.org/wiki/Metaballs) (also known as "blobby objects"), +// which are bounded and closed organic-looking surfaces that meld together when in close proximity. +// +// Includes: +// include +// include +// FileGroup: Advanced Modeling +// FileSummary: Isosurfaces and metaballs. +////////////////////////////////////////////////////////////////////// + + +/* +Lookup Tables for Transvoxel's Modified Marching Cubes + +From https://gist.github.com/dwilliamson/72c60fcd287a94867b4334b42a7888ad + +Unlike the original paper (Marching Cubes: A High Resolution 3D Surface Construction Algorithm), these tables guarantee a closed mesh in which connected components are continuous and free of holes. + +Rotations are prioritized over inversions so that 3 of the 6 cases containing ambiguous faces are never added. 3 extra cases are added as a post-process, overriding inversions through custom-built rotations to eliminate the remaining ambiguities. + +The cube index determines the sequence of edges to split. The index ranges from 0 to 255, representing all possible combinations of the 8 corners of the cube being greater or less than the isosurface threshold. For example, 10000110 (8-bit binary for decimal index 134) has corners 7, 2, and 3 greater than the threshold. After determining the cube's index value, the triangulation order is looked up in a table. + +Axes are: + y + (back) + | z (up) + | / + |/ + +----- x (right) + +Vertex and edge layout (heavier = and # indicate closer to viewer): + + 6 7 + +==========+ +=====6====+ + /# /# /# /# + / # / # 11 7 10 5 + 2 +- # - - - +3 # +- # -2- - + # + | 4+==========+ 5 | +=====4====+ + | / : / 3 8 1 9 + |/ :/ |/ :/ + 0 +----------+ 1 +-----0----+ + +----------------------------------------------------------- +Addition by Alex Matulich: +Vertex and face layout for triangulating one voxel face that corrsesponds to a side of the box bounding all voxels. Corner labels are different to account for rotations assumed in the triangle tables, but the array arrangement is the same, corresponding to indices [x][y][z], with z changing most rapidly. + + 4(back) + 3 +==========+ 7 + /# 5(top) /# + / # / # + 2 +- # - - - +6 # <-- 3 (side) +0(side) --> | 1+==========+ 5 + | / : / + |/ 2(bot) :/ + 0 +----------+ 4 + 1(front) +*/ + +/// four indices for each face of the cube, counterclockwise looking from inside out +_MCFaceVertexIndices = [ + [], + [0,2,3,1], // left, x=0 plane + [0,1,5,4], // front, y=0 plane + [0,4,6,2], // bottom, z=0 plane + [4,5,7,6], // right, x=voxsize plane + [2,6,7,3], // back, y=voxsize plane + [1,3,7,5], // top, z=voxsize plane +]; + +/// return an array of face indices in _MCFaceVertexIndices if the voxel at coordinate v0 corresponds to the bounding box. +function _bbox_faces(v0, voxsize, bbox) = let( + a = v0-bbox[0], + bb1 = bbox[1] - [voxsize,voxsize,voxsize], + b = v0-bb1 +) [ + if(a[0]==0) 1, + if(a[1]==0) 2, + if(a[2]==0) 3, + if(b[0]>=0) 4, + if(b[1]>=0) 5, + if(b[2]>=0) 6 +]; +/// End of bounding-box faace-clipping stuff. Back to the marching cubes triangulation.... + + +/// Pair of vertex indices for each edge on the voxel +_MCEdgeVertexIndices = [ + [0, 1], + [1, 3], + [3, 2], + [2, 0], + [4, 5], + [5, 7], + [7, 6], + [6, 4], + [0, 4], + [1, 5], + [3, 7], + [2, 6], +]; + +/// For each of the 255 configurations of a marching cube, define a list of triangles, specified as triples of edge indices. +_MCTriangleTable = [ + [], + [3,8,0], + [1,0,9], + [9,1,8,8,1,3], + [3,2,11], + [2,11,0,0,11,8], + [1,0,9,3,2,11], + [11,1,2,11,9,1,11,8,9], + [10,2,1], + [2,1,10,0,3,8], + [0,9,2,2,9,10], + [8,2,3,8,10,2,8,9,10], + [1,10,3,3,10,11], + [10,0,1,10,8,0,10,11,8], + [9,3,0,9,11,3,9,10,11], + [9,10,8,8,10,11], + [7,4,8], + [0,3,4,4,3,7], + [0,9,1,4,8,7], + [1,4,9,1,7,4,1,3,7], + [11,3,2,8,7,4], + [4,11,7,4,2,11,4,0,2], + [3,2,11,0,9,1,4,8,7], + [9,1,4,4,1,7,7,1,2,7,2,11], + [7,4,8,1,10,2], + [7,4,3,3,4,0,10,2,1], + [10,2,9,9,2,0,7,4,8], + [7,4,9,7,9,2,9,10,2,3,7,2], + [1,10,3,3,10,11,4,8,7], + [4,0,7,0,1,10,7,0,10,7,10,11], + [7,4,8,9,3,0,9,11,3,9,10,11], + [7,4,11,4,9,11,9,10,11], + [5,9,4], + [8,0,3,9,4,5], + [1,0,5,5,0,4], + [5,8,4,5,3,8,5,1,3], + [3,2,11,5,9,4], + [2,11,0,0,11,8,5,9,4], + [4,5,0,0,5,1,11,3,2], + [11,8,2,8,4,5,2,8,5,2,5,1], + [5,9,4,1,10,2], + [0,3,8,1,10,2,5,9,4], + [2,5,10,2,4,5,2,0,4], + [4,5,8,8,5,3,3,5,10,3,10,2], + [11,3,10,10,3,1,4,5,9], + [4,5,9,10,0,1,10,8,0,10,11,8], + [4,5,10,4,10,3,10,11,3,0,4,3], + [4,5,8,5,10,8,10,11,8], + [5,9,7,7,9,8], + [3,9,0,3,5,9,3,7,5], + [7,0,8,7,1,0,7,5,1], + [3,7,1,1,7,5], + [5,9,7,7,9,8,2,11,3], + [5,9,0,5,0,11,0,2,11,7,5,11], + [2,11,3,7,0,8,7,1,0,7,5,1], + [2,11,1,11,7,1,7,5,1], + [8,7,9,9,7,5,2,1,10], + [10,2,1,3,9,0,3,5,9,3,7,5], + [2,0,10,0,8,7,10,0,7,10,7,5], + [10,2,5,2,3,5,3,7,5], + [5,9,8,5,8,7,1,10,3,10,11,3], + [1,10,0,0,10,11,0,11,7,0,7,5,0,5,9], + [8,7,0,0,7,5,0,5,10,0,10,11,0,11,3], + [5,11,7,10,11,5], + [11,6,7], + [3,8,0,7,11,6], + [1,0,9,7,11,6], + [9,1,8,8,1,3,6,7,11], + [6,7,2,2,7,3], + [0,7,8,0,6,7,0,2,6], + [6,7,2,2,7,3,9,1,0], + [9,1,2,9,2,7,2,6,7,8,9,7], + [10,2,1,11,6,7], + [2,1,10,3,8,0,7,11,6], + [0,9,2,2,9,10,7,11,6], + [6,7,11,8,2,3,8,10,2,8,9,10], + [7,10,6,7,1,10,7,3,1], + [1,10,0,0,10,8,8,10,6,8,6,7], + [9,10,0,10,6,7,0,10,7,0,7,3], + [6,7,10,7,8,10,8,9,10], + [4,8,6,6,8,11], + [6,3,11,6,0,3,6,4,0], + [11,6,8,8,6,4,1,0,9], + [6,4,11,4,9,1,11,4,1,11,1,3], + [2,8,3,2,4,8,2,6,4], + [0,2,4,4,2,6], + [9,1,0,2,8,3,2,4,8,2,6,4], + [9,1,4,1,2,4,2,6,4], + [4,8,6,6,8,11,1,10,2], + [1,10,2,6,3,11,6,0,3,6,4,0], + [0,9,10,0,10,2,4,8,6,8,11,6], + [11,6,3,3,6,4,3,4,9,3,9,10,3,10,2], + [1,10,6,1,6,8,6,4,8,3,1,8], + [1,10,0,10,6,0,6,4,0], + [0,9,3,3,9,10,3,10,6,3,6,4,3,4,8], + [4,10,6,9,10,4], + [4,5,9,6,7,11], + [7,11,6,8,0,3,9,4,5], + [1,0,5,5,0,4,11,6,7], + [11,6,7,5,8,4,5,3,8,5,1,3], + [3,2,7,7,2,6,9,4,5], + [5,9,4,0,7,8,0,6,7,0,2,6], + [1,0,4,1,4,5,3,2,7,2,6,7], + [4,5,8,8,5,1,8,1,2,8,2,6,8,6,7], + [6,7,11,5,9,4,1,10,2], + [5,9,4,7,11,6,0,3,8,2,1,10], + [7,11,6,2,5,10,2,4,5,2,0,4], + [6,7,11,3,8,4,3,4,5,3,5,2,2,5,10], + [9,4,5,7,10,6,7,1,10,7,3,1], + [5,9,4,8,0,1,8,1,10,8,10,7,7,10,6], + [6,7,10,10,7,3,10,3,0,10,0,4,10,4,5], + [4,5,8,8,5,10,8,10,6,8,6,7], + [9,6,5,9,11,6,9,8,11], + [0,3,9,9,3,5,5,3,11,5,11,6], + [1,0,8,1,8,6,8,11,6,5,1,6], + [11,6,3,6,5,3,5,1,3], + [2,6,3,6,5,9,3,6,9,3,9,8], + [5,9,6,9,0,6,0,2,6], + [3,2,8,8,2,6,8,6,5,8,5,1,8,1,0], + [1,6,5,2,6,1], + [2,1,10,9,6,5,9,11,6,9,8,11], + [2,1,10,5,9,0,5,0,3,5,3,6,6,3,11], + [10,2,5,5,2,0,5,0,8,5,8,11,5,11,6], + [10,2,5,5,2,3,5,3,11,5,11,6], + [5,9,6,6,9,8,6,8,3,6,3,1,6,1,10], + [5,9,6,6,9,0,6,0,1,6,1,10], + [8,3,0,5,10,6], + [6,5,10], + [6,10,5], + [3,8,0,5,6,10], + [9,1,0,10,5,6], + [3,8,1,1,8,9,6,10,5], + [6,10,5,2,11,3], + [8,0,11,11,0,2,5,6,10], + [10,5,6,1,0,9,3,2,11], + [5,6,10,11,1,2,11,9,1,11,8,9], + [2,1,6,6,1,5], + [5,6,1,1,6,2,8,0,3], + [6,9,5,6,0,9,6,2,0], + [8,9,3,9,5,6,3,9,6,3,6,2], + [3,6,11,3,5,6,3,1,5], + [5,6,11,5,11,0,11,8,0,1,5,0], + [0,9,3,3,9,11,11,9,5,11,5,6], + [5,6,9,6,11,9,11,8,9], + [7,4,8,5,6,10], + [0,3,4,4,3,7,10,5,6], + [4,8,7,9,1,0,10,5,6], + [6,10,5,1,4,9,1,7,4,1,3,7], + [11,3,2,7,4,8,5,6,10], + [10,5,6,4,11,7,4,2,11,4,0,2], + [7,4,8,3,2,11,9,1,0,10,5,6], + [10,5,6,7,4,9,7,9,1,7,1,11,11,1,2], + [2,1,6,6,1,5,8,7,4], + [7,4,0,7,0,3,5,6,1,6,2,1], + [8,7,4,6,9,5,6,0,9,6,2,0], + [5,6,9,9,6,2,9,2,3,9,3,7,9,7,4], + [4,8,7,3,6,11,3,5,6,3,1,5], + [7,4,11,11,4,0,11,0,1,11,1,5,11,5,6], + [4,8,7,11,3,0,11,0,9,11,9,6,6,9,5], + [5,6,9,9,6,11,9,11,7,9,7,4], + [9,4,10,10,4,6], + [6,10,4,4,10,9,3,8,0], + [0,10,1,0,6,10,0,4,6], + [3,8,4,3,4,10,4,6,10,1,3,10], + [9,4,10,10,4,6,3,2,11], + [8,0,2,8,2,11,9,4,10,4,6,10], + [11,3,2,0,10,1,0,6,10,0,4,6], + [2,11,1,1,11,8,1,8,4,1,4,6,1,6,10], + [4,1,9,4,2,1,4,6,2], + [3,8,0,4,1,9,4,2,1,4,6,2], + [4,6,0,0,6,2], + [3,8,2,8,4,2,4,6,2], + [3,1,11,1,9,4,11,1,4,11,4,6], + [9,4,1,1,4,6,1,6,11,1,11,8,1,8,0], + [11,3,6,3,0,6,0,4,6], + [8,6,11,4,6,8], + [10,7,6,10,8,7,10,9,8], + [10,9,6,9,0,3,6,9,3,6,3,7], + [8,7,0,0,7,1,1,7,6,1,6,10], + [6,10,7,10,1,7,1,3,7], + [3,2,11,10,7,6,10,8,7,10,9,8], + [6,10,7,7,10,9,7,9,0,7,0,2,7,2,11], + [11,3,2,1,0,8,1,8,7,1,7,10,10,7,6], + [6,10,7,7,10,1,7,1,2,7,2,11], + [8,7,6,8,6,1,6,2,1,9,8,1], + [0,3,9,9,3,7,9,7,6,9,6,2,9,2,1], + [8,7,0,7,6,0,6,2,0], + [7,2,3,6,2,7], + [11,3,6,6,3,1,6,1,9,6,9,8,6,8,7], + [11,7,6,1,9,0], + [11,3,6,6,3,0,6,0,8,6,8,7], + [11,7,6], + [10,5,11,11,5,7], + [10,5,11,11,5,7,0,3,8], + [7,11,5,5,11,10,0,9,1], + [3,8,9,3,9,1,7,11,5,11,10,5], + [5,2,10,5,3,2,5,7,3], + [0,2,8,2,10,5,8,2,5,8,5,7], + [0,9,1,5,2,10,5,3,2,5,7,3], + [10,5,2,2,5,7,2,7,8,2,8,9,2,9,1], + [1,11,2,1,7,11,1,5,7], + [8,0,3,1,11,2,1,7,11,1,5,7], + [0,9,5,0,5,11,5,7,11,2,0,11], + [3,8,2,2,8,9,2,9,5,2,5,7,2,7,11], + [5,7,1,1,7,3], + [8,0,7,0,1,7,1,5,7], + [0,9,3,9,5,3,5,7,3], + [9,7,8,5,7,9], + [8,5,4,8,10,5,8,11,10], + [10,5,4,10,4,3,4,0,3,11,10,3], + [1,0,9,8,5,4,8,10,5,8,11,10], + [9,1,4,4,1,3,4,3,11,4,11,10,4,10,5], + [10,5,2,2,5,3,3,5,4,3,4,8], + [10,5,2,5,4,2,4,0,2], + [9,1,0,3,2,10,3,10,5,3,5,8,8,5,4], + [10,5,2,2,5,4,2,4,9,2,9,1], + [1,5,2,5,4,8,2,5,8,2,8,11], + [2,1,11,11,1,5,11,5,4,11,4,0,11,0,3], + [4,8,5,5,8,11,5,11,2,5,2,0,5,0,9], + [5,4,9,2,3,11], + [4,8,5,8,3,5,3,1,5], + [0,5,4,1,5,0], + [0,9,3,3,9,5,3,5,4,3,4,8], + [5,4,9], + [11,4,7,11,9,4,11,10,9], + [0,3,8,11,4,7,11,9,4,11,10,9], + [0,4,1,4,7,11,1,4,11,1,11,10], + [7,11,4,4,11,10,4,10,1,4,1,3,4,3,8], + [9,4,7,9,7,2,7,3,2,10,9,2], + [8,0,7,7,0,2,7,2,10,7,10,9,7,9,4], + [1,0,10,10,0,4,10,4,7,10,7,3,10,3,2], + [7,8,4,10,1,2], + [9,4,1,1,4,2,2,4,7,2,7,11], + [8,0,3,2,1,9,2,9,4,2,4,11,11,4,7], + [7,11,4,11,2,4,2,0,4], + [3,8,2,2,8,4,2,4,7,2,7,11], + [9,4,1,4,7,1,7,3,1], + [9,4,1,1,4,7,1,7,8,1,8,0], + [3,4,7,0,4,3], + [7,8,4], + [8,11,9,9,11,10], + [0,3,9,3,11,9,11,10,9], + [1,0,10,0,8,10,8,11,10], + [10,3,11,1,3,10], + [3,2,8,2,10,8,10,9,8], + [9,2,10,0,2,9], + [1,0,10,10,0,8,10,8,3,10,3,2], + [2,10,1], + [2,1,11,1,9,11,9,8,11], + [2,1,11,11,1,9,11,9,0,11,0,3], + [11,0,8,2,0,11], + [3,11,2], + [1,8,3,9,8,1], + [1,9,0], + [8,3,0], + [], +]; + +/// Same list as above, but with each row in reverse order. Needed for generating shells (two isosurfaces at slightly different iso values). +/// More efficient just to have a static table than to generate it each time by calling reverse() hundreds of times (although this static table was generated that way). +_MCTriangleTable_reverse = [ + [], + [0,8,3], + [9,0,1], + [3,1,8,8,1,9], + [11,2,3], + [8,11,0,0,11,2], + [11,2,3,9,0,1], + [9,8,11,1,9,11,2,1,11], + [1,2,10], + [8,3,0,10,1,2], + [10,9,2,2,9,0], + [10,9,8,2,10,8,3,2,8], + [11,10,3,3,10,1], + [8,11,10,0,8,10,1,0,10], + [11,10,9,3,11,9,0,3,9], + [11,10,8,8,10,9], + [8,4,7], + [7,3,4,4,3,0], + [7,8,4,1,9,0], + [7,3,1,4,7,1,9,4,1], + [4,7,8,2,3,11], + [2,0,4,11,2,4,7,11,4], + [7,8,4,1,9,0,11,2,3], + [11,2,7,2,1,7,7,1,4,4,1,9], + [2,10,1,8,4,7], + [1,2,10,0,4,3,3,4,7], + [8,4,7,0,2,9,9,2,10], + [2,7,3,2,10,9,2,9,7,9,4,7], + [7,8,4,11,10,3,3,10,1], + [11,10,7,10,0,7,10,1,0,7,0,4], + [11,10,9,3,11,9,0,3,9,8,4,7], + [11,10,9,11,9,4,11,4,7], + [4,9,5], + [5,4,9,3,0,8], + [4,0,5,5,0,1], + [3,1,5,8,3,5,4,8,5], + [4,9,5,11,2,3], + [4,9,5,8,11,0,0,11,2], + [2,3,11,1,5,0,0,5,4], + [1,5,2,5,8,2,5,4,8,2,8,11], + [2,10,1,4,9,5], + [4,9,5,2,10,1,8,3,0], + [4,0,2,5,4,2,10,5,2], + [2,10,3,10,5,3,3,5,8,8,5,4], + [9,5,4,1,3,10,10,3,11], + [8,11,10,0,8,10,1,0,10,9,5,4], + [3,4,0,3,11,10,3,10,4,10,5,4], + [8,11,10,8,10,5,8,5,4], + [8,9,7,7,9,5], + [5,7,3,9,5,3,0,9,3], + [1,5,7,0,1,7,8,0,7], + [5,7,1,1,7,3], + [3,11,2,8,9,7,7,9,5], + [11,5,7,11,2,0,11,0,5,0,9,5], + [1,5,7,0,1,7,8,0,7,3,11,2], + [1,5,7,1,7,11,1,11,2], + [10,1,2,5,7,9,9,7,8], + [5,7,3,9,5,3,0,9,3,1,2,10], + [5,7,10,7,0,10,7,8,0,10,0,2], + [5,7,3,5,3,2,5,2,10], + [3,11,10,3,10,1,7,8,5,8,9,5], + [9,5,0,5,7,0,7,11,0,11,10,0,0,10,1], + [3,11,0,11,10,0,10,5,0,5,7,0,0,7,8], + [5,11,10,7,11,5], + [7,6,11], + [6,11,7,0,8,3], + [6,11,7,9,0,1], + [11,7,6,3,1,8,8,1,9], + [3,7,2,2,7,6], + [6,2,0,7,6,0,8,7,0], + [0,1,9,3,7,2,2,7,6], + [7,9,8,7,6,2,7,2,9,2,1,9], + [7,6,11,1,2,10], + [6,11,7,0,8,3,10,1,2], + [6,11,7,10,9,2,2,9,0], + [10,9,8,2,10,8,3,2,8,11,7,6], + [1,3,7,10,1,7,6,10,7], + [7,6,8,6,10,8,8,10,0,0,10,1], + [3,7,0,7,10,0,7,6,10,0,10,9], + [10,9,8,10,8,7,10,7,6], + [11,8,6,6,8,4], + [0,4,6,3,0,6,11,3,6], + [9,0,1,4,6,8,8,6,11], + [3,1,11,1,4,11,1,9,4,11,4,6], + [4,6,2,8,4,2,3,8,2], + [6,2,4,4,2,0], + [4,6,2,8,4,2,3,8,2,0,1,9], + [4,6,2,4,2,1,4,1,9], + [2,10,1,11,8,6,6,8,4], + [0,4,6,3,0,6,11,3,6,2,10,1], + [6,11,8,6,8,4,2,10,0,10,9,0], + [2,10,3,10,9,3,9,4,3,4,6,3,3,6,11], + [8,1,3,8,4,6,8,6,1,6,10,1], + [0,4,6,0,6,10,0,10,1], + [8,4,3,4,6,3,6,10,3,10,9,3,3,9,0], + [4,10,9,6,10,4], + [11,7,6,9,5,4], + [5,4,9,3,0,8,6,11,7], + [7,6,11,4,0,5,5,0,1], + [3,1,5,8,3,5,4,8,5,7,6,11], + [5,4,9,6,2,7,7,2,3], + [6,2,0,7,6,0,8,7,0,4,9,5], + [7,6,2,7,2,3,5,4,1,4,0,1], + [7,6,8,6,2,8,2,1,8,1,5,8,8,5,4], + [2,10,1,4,9,5,11,7,6], + [10,1,2,8,3,0,6,11,7,4,9,5], + [4,0,2,5,4,2,10,5,2,6,11,7], + [10,5,2,2,5,3,5,4,3,4,8,3,11,7,6], + [1,3,7,10,1,7,6,10,7,5,4,9], + [6,10,7,7,10,8,10,1,8,1,0,8,4,9,5], + [5,4,10,4,0,10,0,3,10,3,7,10,10,7,6], + [7,6,8,6,10,8,10,5,8,8,5,4], + [11,8,9,6,11,9,5,6,9], + [6,11,5,11,3,5,5,3,9,9,3,0], + [6,1,5,6,11,8,6,8,1,8,0,1], + [3,1,5,3,5,6,3,6,11], + [8,9,3,9,6,3,9,5,6,3,6,2], + [6,2,0,6,0,9,6,9,5], + [0,1,8,1,5,8,5,6,8,6,2,8,8,2,3], + [1,6,2,5,6,1], + [11,8,9,6,11,9,5,6,9,10,1,2], + [11,3,6,6,3,5,3,0,5,0,9,5,10,1,2], + [6,11,5,11,8,5,8,0,5,0,2,5,5,2,10], + [6,11,5,11,3,5,3,2,5,5,2,10], + [10,1,6,1,3,6,3,8,6,8,9,6,6,9,5], + [10,1,6,1,0,6,0,9,6,6,9,5], + [6,10,5,0,3,8], + [10,5,6], + [5,10,6], + [10,6,5,0,8,3], + [6,5,10,0,1,9], + [5,10,6,9,8,1,1,8,3], + [3,11,2,5,10,6], + [10,6,5,2,0,11,11,0,8], + [11,2,3,9,0,1,6,5,10], + [9,8,11,1,9,11,2,1,11,10,6,5], + [5,1,6,6,1,2], + [3,0,8,2,6,1,1,6,5], + [0,2,6,9,0,6,5,9,6], + [2,6,3,6,9,3,6,5,9,3,9,8], + [5,1,3,6,5,3,11,6,3], + [0,5,1,0,8,11,0,11,5,11,6,5], + [6,5,11,5,9,11,11,9,3,3,9,0], + [9,8,11,9,11,6,9,6,5], + [10,6,5,8,4,7], + [6,5,10,7,3,4,4,3,0], + [6,5,10,0,1,9,7,8,4], + [7,3,1,4,7,1,9,4,1,5,10,6], + [10,6,5,8,4,7,2,3,11], + [2,0,4,11,2,4,7,11,4,6,5,10], + [6,5,10,0,1,9,11,2,3,8,4,7], + [2,1,11,11,1,7,1,9,7,9,4,7,6,5,10], + [4,7,8,5,1,6,6,1,2], + [1,2,6,1,6,5,3,0,7,0,4,7], + [0,2,6,9,0,6,5,9,6,4,7,8], + [4,7,9,7,3,9,3,2,9,2,6,9,9,6,5], + [5,1,3,6,5,3,11,6,3,7,8,4], + [6,5,11,5,1,11,1,0,11,0,4,11,11,4,7], + [5,9,6,6,9,11,9,0,11,0,3,11,7,8,4], + [4,7,9,7,11,9,11,6,9,9,6,5], + [6,4,10,10,4,9], + [0,8,3,9,10,4,4,10,6], + [6,4,0,10,6,0,1,10,0], + [10,3,1,10,6,4,10,4,3,4,8,3], + [11,2,3,6,4,10,10,4,9], + [10,6,4,10,4,9,11,2,8,2,0,8], + [6,4,0,10,6,0,1,10,0,2,3,11], + [10,6,1,6,4,1,4,8,1,8,11,1,1,11,2], + [2,6,4,1,2,4,9,1,4], + [2,6,4,1,2,4,9,1,4,0,8,3], + [2,6,0,0,6,4], + [2,6,4,2,4,8,2,8,3], + [6,4,11,4,1,11,4,9,1,11,1,3], + [0,8,1,8,11,1,11,6,1,6,4,1,1,4,9], + [6,4,0,6,0,3,6,3,11], + [8,6,4,11,6,8], + [8,9,10,7,8,10,6,7,10], + [7,3,6,3,9,6,3,0,9,6,9,10], + [10,6,1,6,7,1,1,7,0,0,7,8], + [7,3,1,7,1,10,7,10,6], + [8,9,10,7,8,10,6,7,10,11,2,3], + [11,2,7,2,0,7,0,9,7,9,10,7,7,10,6], + [6,7,10,10,7,1,7,8,1,8,0,1,2,3,11], + [11,2,7,2,1,7,1,10,7,7,10,6], + [1,8,9,1,2,6,1,6,8,6,7,8], + [1,2,9,2,6,9,6,7,9,7,3,9,9,3,0], + [0,2,6,0,6,7,0,7,8], + [7,2,6,3,2,7], + [7,8,6,8,9,6,9,1,6,1,3,6,6,3,11], + [0,9,1,6,7,11], + [7,8,6,8,0,6,0,3,6,6,3,11], + [6,7,11], + [7,5,11,11,5,10], + [8,3,0,7,5,11,11,5,10], + [1,9,0,10,11,5,5,11,7], + [5,10,11,5,11,7,1,9,3,9,8,3], + [3,7,5,2,3,5,10,2,5], + [7,5,8,5,2,8,5,10,2,8,2,0], + [3,7,5,2,3,5,10,2,5,1,9,0], + [1,9,2,9,8,2,8,7,2,7,5,2,2,5,10], + [7,5,1,11,7,1,2,11,1], + [7,5,1,11,7,1,2,11,1,3,0,8], + [11,0,2,11,7,5,11,5,0,5,9,0], + [11,7,2,7,5,2,5,9,2,9,8,2,2,8,3], + [3,7,1,1,7,5], + [7,5,1,7,1,0,7,0,8], + [3,7,5,3,5,9,3,9,0], + [9,7,5,8,7,9], + [10,11,8,5,10,8,4,5,8], + [3,10,11,3,0,4,3,4,10,4,5,10], + [10,11,8,5,10,8,4,5,8,9,0,1], + [5,10,4,10,11,4,11,3,4,3,1,4,4,1,9], + [8,4,3,4,5,3,3,5,2,2,5,10], + [2,0,4,2,4,5,2,5,10], + [4,5,8,8,5,3,5,10,3,10,2,3,0,1,9], + [1,9,2,9,4,2,4,5,2,2,5,10], + [11,8,2,8,5,2,8,4,5,2,5,1], + [3,0,11,0,4,11,4,5,11,5,1,11,11,1,2], + [9,0,5,0,2,5,2,11,5,11,8,5,5,8,4], + [11,3,2,9,4,5], + [5,1,3,5,3,8,5,8,4], + [0,5,1,4,5,0], + [8,4,3,4,5,3,5,9,3,3,9,0], + [9,4,5], + [9,10,11,4,9,11,7,4,11], + [9,10,11,4,9,11,7,4,11,8,3,0], + [10,11,1,11,4,1,11,7,4,1,4,0], + [8,3,4,3,1,4,1,10,4,10,11,4,4,11,7], + [2,9,10,2,3,7,2,7,9,7,4,9], + [4,9,7,9,10,7,10,2,7,2,0,7,7,0,8], + [2,3,10,3,7,10,7,4,10,4,0,10,10,0,1], + [2,1,10,4,8,7], + [11,7,2,7,4,2,2,4,1,1,4,9], + [7,4,11,11,4,2,4,9,2,9,1,2,3,0,8], + [4,0,2,4,2,11,4,11,7], + [11,7,2,7,4,2,4,8,2,2,8,3], + [1,3,7,1,7,4,1,4,9], + [0,8,1,8,7,1,7,4,1,1,4,9], + [3,4,0,7,4,3], + [4,8,7], + [10,11,9,9,11,8], + [9,10,11,9,11,3,9,3,0], + [10,11,8,10,8,0,10,0,1], + [10,3,1,11,3,10], + [8,9,10,8,10,2,8,2,3], + [9,2,0,10,2,9], + [2,3,10,3,8,10,8,0,10,10,0,1], + [1,10,2], + [11,8,9,11,9,1,11,1,2], + [3,0,11,0,9,11,9,1,11,11,1,2], + [11,0,2,8,0,11], + [2,11,3], + [1,8,9,3,8,1], + [0,9,1], + [0,3,8], + [] +]; + + +// Function&Module: isosurface() +// Synopsis: Creates a 3D isosurface. +// SynTags: Geom,VNF +// Topics: Advanced Modeling +// Usage: As a module +// isosurface(voxel_size, bounding_box, isovalue, field_function, [additional=], [reverse=], [close_clip=], [show_stats=]); +// Usage: As a function +// vnf = isosurface(voxel_size, bounding_box, isovalue, field_function, [additional=], [close_clip=], [show_stats=]); +// Description: +// When called as a function, returns a [VNF structure](vnf.scad) (list of triangles and faces) representing a 3D isosurface within the specified bounding box at a single isovalue or range of isovalues. +// When called as a module, displays the isosurface within the specified bounding box at a single isovalue or range of isovalues. This module just passes the parameters to the function, and then calls `{{vnf_polyhedron()}}` to display the isosurface. +// . +// A [marching cubes](https://en.wikipedia.org/wiki/Marching_cubes) algorithm is used +// to identify an envelope containing the isosurface within the bounding box. The surface +// intersecttion with a voxel cube is then triangulated to form a surface fragment, which is +// combined with all other surface fragments. Ambiguities in triangulating the surfaces +// in certain voxel cube configurations are resolved so that all triangular facets are +// properly oriented with no holes in the surface. If a side of the bounding box clips +// the isosurface, this clipped area is filled in so that the surface remains manifold. +// . +// Be mindful of how you set `voxel_size` and `bounding_box`. For example a voxel size +// of 1 unit with a bounding box volume of 200×200×200 may be noticeably slow, +// requiring calculation and storage of 8,000,000 field values, and more processing +// and memory to generate the triangulated mesh. On the other hand, a voxel size of 5 +// in a 100×100×100 bounding box requires only 8,000 field values and the mesh +// generates fairly quickly, just a handful of seconds. A good rule is to keep the +// number of field values below 10,000 for preview, and adjust the voxel size +// smaller for final rendering. If the isosurface fits completely within the bounding +// box, you can call `{{pointlist_bounds()}}` on `vnf[0]` returned from the +// `isosurface()` function to get an idea of a more optimal smaller bounding box to use, +// possibly allowing increasing resolution by decresing the voxel size. +// . +// The point list in the VNF structure contains many duplicated points. This is not a +// problem for rendering the shape, but if you want to eliminate these, you can pass +// the structure to {{vnf_merge_points()}}. Additionally, flat surfaces (often +// resulting from clipping by the bounding box) are triangulated at the voxel size +// resolution, and these can be unified into a single face by passing the vnf +// structure to {{vnf_unify_faces()}}. These steps can be expensive for execution time +// and are not normally necessary. +// Arguments: +// voxel_size = The size (scalar) of the voxel cube that determines the resolution of the surface. +// bounding_box = A pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum box corner coordinates. You don't have ensure that the voxels fit perfectly inside the bounding box. While the voxel at the minimum bounding box corner is aligned on that corner, the last voxel at the maximum box corner may extend a bit beyond it. +// isovalue = As a scalar, specifies the output value of `field_function` corresponding to the isosurface. As a vector `[min_isovalue, max_isovalue]`, specifies the range of isovalues around which to generate a surface. For closed surfaces, a single value results in a closed volume, and a range results in a shell (with an inside and outside surface) enclosing a volume. A range must be specified for infinite-extent surfaces (such as gyroids) to create a manifold shape within the bounding box. +// field_function = A [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) taking as input an `[x,y,z]` coordinate and optional additional parameters, and returns a single value. +// --- +// additional = A single value, or an array of optional additional parameters that may be required by the field function. It is your responsibility to create a function literal compatible with these inputs. Nothing is passed to the function literal if `additional` is not set. Default: undef +// reverse = when true, reverses the orientation of the facets in the mesh. Default: false +// close_clip = when true, maintains a manifold surface where the bounding box clips it (there is a nearly negligible speed penalty in doing this). When false, the bounding box clips the surface, exposing the back sides of facets. Setting this to false can be useful with OpenSCAD's "View > Thrown together" menu option to distinguish inside from outside. Default: true +// show_stats = If true, display statistics about the isosurface in the console window. Besides the number of voxels found to contain the surface, and the number of triangles making up the surface, this is useful for getting information about a smaller bounding box possible for the isosurface, to improve speed for subsequent renders. Enabling this parameter has a speed penalty. Default: false +// Example(3D,ThrownTogether,NoAxes): A gyroid is an isosurface defined by all the zero values of a 3D periodic function. To illustrate what the surface looks like, `close_clip=false` has been set to expose both sides of the surface. The surface is periodic and tileable along all three axis directions. This a non-manifold surface as displayed, not useful for 3D modeling. This example also demonstrates the use of the `additional` parameter, which in this case controls the wavelength of the gyroid. +// gyroid = function (xyz, wavelength) let( +// p = 360/wavelength, +// px = p*xyz[0], +// py = p*xyz[1], +// pz = p*xyz[2] +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(voxel_size=5, bounding_box=bbox, isovalue=0, +// gyroid, additional=200, close_clip=false); +// Example(3D,NoAxes): If we remove the `close_clip` parameter or set it to true, the isosurface algorithm encloses the entire half-space bounded by the "inner" gyroid surface, leaving only the "outer" surface exposed. This is a manifold shape but not what we want if trying to model a gyroid. +// gyroid = function (xyz, wavelength) let( +// p = 360/wavelength, +// px = p*xyz[0], +// py = p*xyz[1], +// pz = p*xyz[2] +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(voxel_size=5, bounding_box=bbox, isovalue=0, +// gyroid, additional=200); +// Example(3D,ThrownTogether,NoAxes): To make the gyroid a double-sided surface, we need to specify a small range around zero for `isovalue`. Now we have a double-sided surface although with `clip_close=false` the edges are not closed where the surface is clipped by the bounding box. +// gyroid = function (xyz, wavelength) let( +// p = 360/wavelength, +// px = p*xyz[0], +// py = p*xyz[1], +// pz = p*xyz[2] +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(voxel_size=5, bounding_box=bbox, isovalue=[-0.3, 0.3], +// gyroid, additional=200, close_clip=false); +// Example(3D,ThrownTogether,NoAxes): To make the gyroid a valid manifold 3D object, we remove the `close_clip` parameter (same as setting `close_clip=true`), which closes the edges where the surface is clipped by the bounding box. The resulting object can be tiled, the VNF returned by the functional version can be wrapped around an axis using `{{vnf_bend()}}`, and other operations. +// gyroid = function (xyz, wavelength) let( +// p = 360/wavelength, +// px = p*xyz[0], +// py = p*xyz[1], +// pz = p*xyz[2] +// ) sin(px)*cos(py) + sin(py)*cos(pz) + sin(pz)*cos(px); +// +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(voxel_size=5, bounding_box=bbox, isovalue=[-0.3, 0.3], +// gyroid, additional=200); +// Example(3D,NoAxes): An approximation of the triply-periodic minimal surface known as [Schwartz P](https://en.wikipedia.org/wiki/Schwarz_minimal_surface). +// schwartz_p = function (xyz, wavelength) let( +// p = 360/wavelength, +// px = p*xyz[0], +// py = p*xyz[1], +// pz = p*xyz[2] +// ) cos(px) + cos(py) + cos(pz); +// +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(voxel_size=4, bounding_box=bbox, isovalue=[-0.2,0.2], +// schwartz_p, additional=100); +// Example(3D,NoAxes): Another approximation of the triply-periodic minimal surface known as [Neovius](https://en.wikipedia.org/wiki/Neovius_surface). +// neovius = function (xyz, wavelength) let( +// p = 360/wavelength, +// px = p*xyz[0], +// py = p*xyz[1], +// pz = p*xyz[2] +// ) 3*(cos(px) + cos(py) + cos(pz)) + 4*cos(px)*cos(py)*cos(pz); +// +// bbox = [[-100,-100,-100], [100,100,100]]; +// isosurface(voxel_size=4, bounding_box=bbox, isovalue=[-0.3,0.3], +// neovius, additional=200); + +module isosurface(voxel_size, bounding_box, isovalue, field_function, additional, reverse=false, close_clip=true, show_stats=false) { + vnf = isosurface(voxel_size, bounding_box, isovalue, field_function, additional, reverse, close_clip, show_stats); + vnf_polyhedron(vnf); +} + +function isosurface(voxel_size, bounding_box, isovalue, field_function, additional, reverse=false, close_clip=true, show_stats=false) = + assert(all_defined([voxel_size, bounding_box, isovalue, field_function]), "The parameters voxel_size, bounding_box, isovalue, and field_function must all be defined.") + let( + isovalmin = is_list(isovalue) ? isovalue[0] : isovalue, + isovalmax = is_list(isovalue) ? isovalue[1] : INF, + newbbox = let( // new bounding box quantized for voxel_size + hv = 0.5*voxel_size, + bbn = (bounding_box[1]-bounding_box[0]+[hv,hv,hv]) / voxel_size, + bbsize = [round(bbn[0]), round(bbn[1]), round(bbn[2])] * voxel_size + ) [bounding_box[0], bounding_box[0]+bbsize], + cubes = _isosurface_cubes(voxel_size, bbox=newbbox, fieldfunc=field_function, additional=additional, isovalmin=isovalmin, isovalmax=isovalmax, close_clip=close_clip), + tritablemin = reverse ? _MCTriangleTable_reverse : _MCTriangleTable, + tritablemax = reverse ? _MCTriangleTable : _MCTriangleTable_reverse, + trianglepoints = _isosurface_triangles(cubes, voxel_size, isovalmin, isovalmax, tritablemin, tritablemax), + faces = [ for(i=[0:3:len(trianglepoints)-1]) [i,i+1,i+2] ], + dummy = show_stats ? _showstats(voxel_size, newbbox, isovalmin, cubes, faces) : 0 +) [trianglepoints, faces]; + + +// Function&Module: isosurface_array() +// Synopsis: Creates a 3D isosurface from a 3D array of densities. +// SynTags: Geom,VNF +// Topics: Advanced Modeling +// Usage: As a module +// isosurface_array(voxel_size, isovalue, fields, [reverse=], [close_clip=], [show_stats=]); +// Usage: As a function +// vnf = isosurface_array(voxel_size, isovalue, fields, [reverse=], [close_clip=], [show_stats=]); +// Description: +// When called as a function, returns a [VNF structure](vnf.scad) (list of triangles and +// faces) representing a 3D isosurface within the passed array at a single isovalue or +// range of isovalues. +// When called as a module, displays the isosurface within the passed array at a single +// isovalue or range of isovalues. This module just passes the parameters to the function, +// and then calls `{{vnf_polyhedron()}}` to display the isosurface. +// . +// Use this when you already have a 3D array of intensity or density data, for example like +// what you may get from a CT scan. +// . +// The returned VNF structure occupies a volume with its origin at [0,0,0] extending in the +// positive x, y, and z directions by multiples of `voxel_size`. +// . +// The point list in the VNF structure contains many duplicated points. This is not a +// problem for rendering the shape, but if you want to eliminate these, you can pass +// the structure to {{vnf_merge_points()}}. Additionally, flat surfaces at the outer limits +// of the `fields` array are triangulated at the voxel size +// resolution, and these can be unified into a single face by passing the vnf +// structure to {{vnf_unify_faces()}}. These steps can be expensive for execution time +// and are not normally necessary. +// Arguments: +// voxel_size = The size (scalar) of the voxel cube that determines the resolution of the surface. +// isovalue = As a scalar, specifies the output value of `field_function` corresponding to the isosurface. As a vector `[min_isovalue, max_isovalue]`, specifies the range of isovalues around which to generate a surface. For closed surfaces, a single value results in a closed volume, and a range results in a shell (with an inside and outside surface) enclosing a volume. A range must be specified for infinite-extent surfaces (such as gyroids) to create a manifold shape within the bounding box. +// fields = 3D array of field intesities. This array should be organized so that the indices are in order of x, y, and z when the array is referenced; that is, `fields[x_index][y_index][z_index]` has `z_index` changing most rapidly as the array is traversed. If you organize the array differently, you may have to perform a `rotate()` or `mirror()` operation on the final result to orient it properly. +// --- +// origin = origin in 3D space of the [0,0,0] index of the `fields` array. Default: [0,0,0] +// close_clip = when true, maintains a manifold surface where the bounding box clips it (there is a nearly negligible speed penalty in doing this). When false, the bounding box clips the surface, exposes the back sides of facets. Setting this to false can be useful with OpenSCAD's "View > Thrown together" menu option to distinguish inside from outside. Default: true +// show_stats = If true, display statistics about the isosurface in the console window. Besides the number of voxels found to contain the surface, and the number of triangles making up the surface, this is useful for getting information about a smaller bounding box possible for the isosurface, to improve speed for subsequent renders. Enabling this parameter has a speed penalty. Default: false +// Example(3D): +// fields = [ +// repeat(0,[6,6]), +// [ [0,1,2,2,1,0], +// [1,2,3,3,2,1], +// [2,3,4,4,3,2], +// [2,3,4,4,3,2], +// [1,2,3,3,2,1], +// [0,1,2,2,1,0] +// ], +// [ [0,0,0,0,0,0], +// [0,0,1,1,0,0], +// [0,2,3,3,2,0], +// [0,2,3,3,2,0], +// [0,0,1,1,0,0], +// [0,0,0,0,0,0] +// ], +// [ [0,0,0,0,0,0], +// [0,0,0,0,0,0], +// [0,1,2,2,1,0], +// [0,1,2,2,1,0], +// [0,0,0,0,0,0], +// [0,0,0,0,0,0] +// ], +// repeat(0,[6,6]) +// ]; +// rotate([0,-90,180]) isosurface_array(voxel_size=10, isovalue=0.5, fields=fields); + +module isosurface_array(voxel_size, isovalue, fields, origin=[0,0,0], reverse=false, close_clip=true, show_stats=false) { + vnf = isosurface_array(voxel_size, isovalue, fields, origin, reverse, close_clip, show_stats); + vnf_polyhedron(vnf); +} +function isosurface_array(voxel_size, isovalue, fields, origin=[0,0,0], reverse=false, close_clip=true, show_stats=false) = + assert(all_defined([voxel_size, fields, isovalue]), "The parameters voxel_size, fields, and isovalue must all be defined.") + let( + isovalmin = is_list(isovalue) ? isovalue[0] : isovalue, + isovalmax = is_list(isovalue) ? isovalue[1] : INF, + bbox = let( + nx = len(fields)-1, + ny = len(fields[0])-1, + nz = len(fields[0][0])-1 + ) [origin, origin+[nx*voxel_size, ny*voxel_size, nz*voxel_size]], + cubes = _isosurface_cubes(voxel_size, bbox, fieldarray=fields, isovalmin=isovalmin, isovalmax=isovalmax, close_clip=close_clip), + tritablemin = reverse ? _MCTriangleTable_reverse : _MCTriangleTable, + tritablemax = reverse ? _MCTriangleTable : _MCTriangleTable_reverse, + trianglepoints = _isosurface_triangles(cubes, voxel_size, isovalmin, isovalmax, tritablemin, tritablemax), + faces = [ for(i=[0:3:len(trianglepoints)-1]) [i,i+1,i+2] ], + dummy = show_stats ? _showstats(voxel_size, bbox, isovalmin, cubes, faces) : 0 +) [trianglepoints, faces]; + + +/// isosurface_cubes() - private function, called by isosurface() +/// This implements a marching cube algorithm, sacrificing some memory in favor of speed. +/// Return a list of voxel cube structures that have one or both surfaces isovalmin or isovalmax intersecting them, and cubes inside the isosurface volume that are at the bounds of the bounding box. +/// The cube structure is: +/// [cubecoord, cubeindex_isomin, cubeindex_isomax, field, bfaces] +/// where +/// cubecoord is the [x,y,z] coordinate of the front left bottom corner of the voxel, +/// cubeindex_isomin and cubeindex_isomax are the index IDs of the voxel corresponding to the min and max iso surface intersections +/// cf is vector containing the 6 field strength values at each corner of the voxel cube +/// bfaces is an array of faces corresponding to the sides of the bounding box - this is empty most of the time; it has data only where the isosurface is clipped by the bounding box. +/// The bounding box 'bbox' is expected to be quantized for the voxel size already. + +function _isosurface_cubes(voxsize, bbox, fieldarray, fieldfunc, additional, isovalmin, isovalmax, close_clip=true) = let( + // get field intensities + fields = is_def(fieldarray) + ? fieldarray + : let(v = bbox[0], hv = 0.5*voxsize, b1 = bbox[1]+[hv,hv,hv]) [ + for(x=[v[0]:voxsize:b1[0]]) [ + for(y=[v[1]:voxsize:b1[1]]) [ + for(z=[v[2]:voxsize:b1[2]]) + additional==undef + ? fieldfunc([x,y,z]) + : fieldfunc([x,y,z], additional) + ] + ] + ], + nx = len(fields)-2, + ny = len(fields[0])-2, + nz = len(fields[0][0])-2, + v0 = bbox[0] +) [ + for(i=[0:nx]) let(x=v0[0]+voxsize*i) + for(j=[0:ny]) let(y=v0[1]+voxsize*j) + for(k=[0:nz]) let(z=v0[2]+voxsize*k) + let(i1=i+1, j1=j+1, k1=k+1, + cf = [ + fields[i][j][k], + fields[i][j][k1], + fields[i][j1][k], + fields[i][j1][k1], + fields[i1][j][k], + fields[i1][j][k1], + fields[i1][j1][k], + fields[i1][j1][k1] + ], + mincf = min(cf), + maxcf = max(cf), + cubecoord = [x,y,z], + bfaces = close_clip ? _bbox_faces(cubecoord, voxsize, bbox) : [], + cubefound_isomin = (mincf<=isovalmin && isovalmin isoval ? 1 : 0) + + (f[1] > isoval ? 2 : 0) + + (f[2] > isoval ? 4 : 0) + + (f[3] > isoval ? 8 : 0) + + (f[4] > isoval ? 16 : 0) + + (f[5] > isoval ? 32 : 0) + + (f[6] > isoval ? 64 : 0) + + (f[7] > isoval ? 128 : 0); + + +/// _isosurface_trangles() - called by isosurface() +/// Given a list of voxel cubes structures, triangulate the isosurface(s) that intersect each cube and return a list of triangle vertices. +function _isosurface_triangles(cubelist, cubesize, isovalmin, isovalmax, tritablemin, tritablemax) = [ + for(cl=cubelist) let( + v = cl[0], + cbidxmin = cl[1], + cbidxmax = cl[2], + f = cl[3], + bbfaces = cl[4], + vcube = [ + v, v+[0,0,cubesize], v+[0,cubesize,0], v+[0,cubesize,cubesize], + v+[cubesize,0,0], v+[cubesize,0,cubesize], + v+[cubesize,cubesize,0], v+[cubesize,cubesize,cubesize] + ], + epathmin = tritablemin[cbidxmin], + epathmax = tritablemax[cbidxmax], + lenmin = len(epathmin), + lenmax = len(epathmax), + outfacevertices = flatten([ + for(bf = bbfaces) + _bbfacevertices(vcube, f, bf, isovalmax, isovalmin) + ]), + n_outer = len(outfacevertices) + ) + // bunch of repeated code here in an attempt to gain some speed to avoid function calls and calls to flatten(). + // Where the face of the bounding box clips a voxel, those are done in separate if() blocks and require require a concat(), but the majority of voxels can have triangles generated directly. If there is no clipping, the list of trianges is generated all at once. + if(lenmin>0 && lenmax>0) let( + // both min and max surfaces intersect a voxel clipped by bounding box + list = concat( + // min surface + [ for(ei=epathmin) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], + // max surface + [ for(ei=epathmax) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) + ) for(ls = list) ls + else if(n_outer>0 && lenmin>0) let( + // only min surface intersects a voxel clipped by bounding box + list = concat( + [ for(ei=epathmin) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) + ) for(ls = list) ls + else if(lenmin>0) + // only min surface intersects a voxel + for(ei=epathmin) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) + else if(n_outer>0 && lenmax>0) let( + // only max surface intersects the voxel on the bounding box + list = concat( + [ for(ei=epathmax) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) ], outfacevertices) + ) for(ls = list) ls + else if(lenmax>0) + // only max surface intersects the voxel + for(ei=epathmax) let( + edge = _MCEdgeVertexIndices[ei], + vi0 = edge[0], + vi1 = edge[1], + denom = f[vi1] - f[vi0], + u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f[vi0]) / denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) + else if(n_outer>0) + // no surface intersects a voxel clipped by bounding box but the bounding box at this voxel is inside the volume between isomin and isomax + for(ls = outfacevertices) ls +]; + + +/// Generate triangles for voxel faces clipped by the bounding box +function _bbfacevertices(vcube, f, bbface, isovalmax, isovalmin) = let( + vi = _MCFaceVertexIndices[bbface], + vfc = [ for(i=vi) vcube[i] ], + fld = [ for(i=vi) f[i] ], + pgon = flatten([ + for(i=[0:3]) let( + vi0=vi[i], + vi1=vi[(i+1)%4], + f0 = f[vi0], + f1 = f[vi1], + lowhiorder = (f0=f1) let( + u = abs(denom)<0.0001 ? 0.5 : (isovalmax-f0)/denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]), + if(fbetweenlow && f0>=f1) let( + u = abs(denom)<0.0001 ? 0.5 : (isovalmin-f0)/denom + ) vcube[vi0] + u*(vcube[vi1]-vcube[vi0]) + + ] + ]), + npgon = len(pgon), + triangles = npgon==0 ? [] : [ + for(i=[1:len(pgon)-2]) [pgon[0], pgon[i], pgon[i+1]] + ]) flatten(triangles); + + +/// _showstats() (Private function) - called by isosurface() and isosurface_array() +/// Display statistics about isosurface +function _showstats(voxelsize, bbox, isoval, cubes, faces) = let( + v = column(cubes, 0), // extract cube vertices + x = column(v,0), // extract x values + y = column(v,1), // extract y values + z = column(v,2), // extract z values + xmin = min(x), + xmax = max(x)+voxelsize, + ymin = min(y), + ymax = max(y)+voxelsize, + zmin = min(z), + zmax = max(z)+voxelsize, + ntri = len(faces), + nvox = len(cubes) +) echo(str("\nIsosurface statistics:\n Outer isovalue = ", isoval, "\n Voxel size = ", voxelsize, + "\n Voxels found containing surface = ", nvox, "\n Triangles = ", ntri, + "\n Bounding box for all data = ", bbox, + "\n Bounding box for isosurface = ", [[xmin,ymin,zmin], [xmax,ymax,zmax]], + "\n")) 0; + + +/// ---------- metaball stuff starts here, uses isosurface_array() above ---------- + +/// metaball function literal indices + +MB_SPHERE=0; +MB_ELLIPSOID=1; +MB_ROUNDCUBE=2; +MB_CUBE=3; +MB_OCTAHEDRON=4; +MB_TORUS=5; +MB_CUSTOM=6; + +/// Built-in metaball functions corresponding to each MB_ index. +/// Each function takes three parameters: +/// cdist = cartesian distance, a vector [dx,dy,dz] being the distances from the ball center to the volume sample point +/// charge = the charge of the metaball, can be a vector if the charges are different on each axis. +/// additional (named whatever's convenient) = additional value or array of values needed by the function. +/// rcutoff = radial cutoff; effect suppression increases with distance until zero at the rcutoff distance, and is zero from that point farther out. Default: INF + +_metaball_sphere = function (cdist, charge, unused, rotm_unused, rcutoff=INF) +let( + r = norm(cdist), + suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a +) r==0 ? 10000*charge : suppress*charge / r; + +_metaball_ellipsoid = function (cdist, charge, unused, rotm, rcutoff=INF) +let( + dist = concat(cdist,1) * rotm, + r = norm([dist[0]/charge[0], dist[1]/charge[1], dist[2]/charge[2]]), + suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a, + sgn = sign(charge[0]*charge[1]*charge[2]) +) r==0 ? 10000*sgn*max(abs(charge[0]), abs(charge[1]), abs(charge[2])) + : suppress*sgn / r; + +_metaball_roundcube = function (cdist, charge, exponent, rotm, rcutoff=INF) +let( + dist = concat(cdist,1) * rotm, + r = abs(dist[0])^exponent + abs(dist[1])^exponent + abs(dist[2])^exponent, + suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a +) r==0 ? 10000*charge : suppress*sign(charge)*abs(charge)^exponent / r; + +_metaball_cube = function (cdist, charge, unused, rotm, rcutoff=INF) +let( + dist = concat(cdist,1) * rotm, + r = max(abs(dist[0]), abs(dist[1]), abs(dist[2])), + suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a +) r==0 ? 10000*charge : suppress*sign(charge)*abs(charge) / r; + +_metaball_octahedron = function (cdist, charge, unused, rotm, rcutoff=INF) +let( + dist = concat(cdist,1) * rotm, + r = abs(dist[0]) + abs(dist[1]) + abs(dist[2]), + suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a +) r==0 ? 10000*charge : suppress*sign(charge)*abs(charge) / r; + +_metaball_torus = function (cdist, charge, axis, rotm, rcutoff=INF) +let( + tmp = concat(cdist,1) * rotm, + dist = [tmp[0], tmp[1], tmp[2]], + bigdia = abs(charge[0]), + smalldia = charge[1], + d_axisplane = norm(v_mul([1,1,1]-axis, dist)) - bigdia, + d_axis = axis*dist, + r = norm([d_axisplane, d_axis]), + suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a +) r==0 ? 1000*max(charge) : suppress*sign(charge[0])*smalldia / r; + + +/// metaball field function, calling any of the other metaball functions above to accumulate +/// the contribution of each metaball at point xyz +_metaball_fieldfunc = function(xyz, nballs, ball_centers, charges, ball_type, rotmatrix, additional, rcutoff, funcs) +let( + contrib = [ + for(i=[0:nballs-1]) let( + dist = xyz-ball_centers[i], + func = ball_type[i]==MB_CUSTOM ? funcs[MB_CUSTOM][i] : funcs[ball_type[i]] + ) func(dist, charges[i], additional[i], rotmatrix[i], rcutoff[i]) + ] +) sum(contrib); + + +// Function&Module: metaballs() +// Synopsis: Creates a model of metaballs within a bounding box. +// SynTags: Geom +// Topics: Advanced Modeling +// Usage: As a module +// metaballs(voxel_size, bounding_box, isovalue, ball_centers, [ball_sizes=], [ball_type=], [rotation=], [field_function=], [radial_cutoff=], [close_clip=], [show_stats=]); +// Usage: As a function +// vnf = metaballs(voxel_size, bounding_box, isovalue, ball_centers, [ball_sizes=], [ball_type=], [rotation=], [field_function=], [radial_cutoff=], [close_clip=], [show_stats=]); +// Description: +// [Metaballs](https://en.wikipedia.org/wiki/Metaballs), also known as "blobby objects", +// are organic-looking ball-shaped blobs that meld together when in close proximity. +// The melding property is determined by an interaction formula based on the "charge" of +// each ball and their distance from one another. If you consider a "ball" to be a point +// charge in 3D space, the "charge" of a ball is the electric field surrounding that +// charge, and the metaball is the isosurface corresponding to a constant field value. +// The stronger the charge, the stronger the electric field, and the farther away a +// surface of constant field intensity is from the ball center. +// . +// In physics, the electric field intensity falls off as an inverse-square relationship +// with distance; that is, the field is proportional to 1/r^2 where r is the radial +// distance from the point charge. However, most implementations of metaballs instead use +// a simple inverse relationship proportional to 1/r. That is true for most of the field +// types available here, or you can define your own falloff function as the +// `field_function` parameter. +// . +// Six shapes of fields around each metaball center are possible. You can specify +// different types for each metaball in the list, and you can also specify your own +// custom field equation. The five types are: +// * `MB_SPHERE` - the standard spherical metaball with a 1/r field strength falloff. +// * `MB_ELLIPSOID` - an ellipsoid-shaped field that requires specifying a [x,y,z] vector for the charge, representing field strength in each of the x, y, and z directions +// * `MB_ROUNDCUBE` - a cube-shaped metaball with corners that get more rounded with size. The squareness can be controlled with a value between 0 (spherical) or 1 (cubical) in the `additional` parameter, and defaults to 0.5 if omitted. +// * `MB_CUBE` - a cube-shaped metaball with sharp edges and corners, resulting from using [Chebyshev distance](https://en.wikipedia.org/wiki/Chebyshev_distance) rather than Euclidean distance calculations. +// * `MB_OCTAHEDRON` - an octahedron-shaped metaball with sharp edges and corners, resulting from using [taxicab distance](https://en.wikipedia.org/wiki/Taxicab_geometry) rather than Euclidean distance calculations. +// * `MB_TORUS` - a toroidal field oriented perpendicular to the x, y, or z axis. The `charge` is a two-element vector determining the major and minor diameters, and the `additional` paramater sets the axis directions for each ball center (defaults to [0,0,1] if not set). +// * `MB_CUSTOM` - your own custom field definition, requiring you to set the `field_function` parameter to your own function literal. +// If either MB_ELLIPSOID or MB_TORUS occur in the list, the list of charges **must** be explicitly defined rather than supplying a single value for all. +// Arguments: +// voxel_size = The size (scalar) of the voxel cube that determines the resolution of the metaball surface. +// bounding_box = A pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]`, specifying the minimum and maximum box corner coordinates. The voxels needn't fit perfectly inside the bounding box. +// isovalue = A scalar value specifying the isosurface value of the metaballs. +// ball_centers = an array of 3D points specifying each of the metaball centers. +// --- +// charge = a single value, or an array of values corresponding to `ball_centers`, specifying the charge intensity of each ball center. Default: 10 +// ball_type = shape of field that falls off from the metaball center. Can be one of `MB_SPHERE`, `MB_ELLIPSOID`, `MB_ROUNDCUBE, `MB_CUBE`, `MB_OCTAHEDRON`, `MB_TORUS`, or `MB_CUSTOM`. This may be an array of values corresponding to each ball. Where this value is `MB_CUSTOM`, the corresponding array element in `field_function` must also be set. Default: _MB_SPHERE +// rotation = A vector `[x_rotation, y_rotation, z_rotation]`, or list of vectors for each ball, specifying the rotation angle in degrees around the x, y, and z axes. This is meaningless for `_MB_SPHERE` but allows you to orient the other metaball types. Default: undef +// field_function = A single [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals) or array of function literals that return a single field value from one metaball, and takes as inputs a 3D distance vector, a single charge or list of charges, and a single additional parameter or list of parameters (that third parameter must exist even if it isn't used). If the corresponding `ball_type` parameter is not `MB_CUSTOM`, then the function specified in `ball_type` is used instead; only where `ball_type` is `MB_CUSTOM` does this custom field function get invoked. Default: undef +// additional = A single value, or a list of optional additional parameters that may be required by the field function. If you make a custom function, it is your responsibility to create a function literal compatible with these inputs. Nothing is passed to the function literal if `additional` is not set. This parameter must be specified as an entire list for all metaballs if MB_ELLIPSOID or MB_TORUS is included in `ball_type`. Default: `undef` for `ball_type=CUSTOM` +// radial_cutoff = Maximum radial distance of a metaball's influence. This isn't a sharp cutoff; rather, the suppression increases with distance until the influence is zero at the `radial_cutoff` distance. Can be a single value or an array of values corresponding to each ball center, but typically it's sufficient to supply a single value approximately the average separation of each ball, so each ball mostly acts on its nearest neighbors. Default: INF +// close_clip = when true, maintains a manifold surface where the bounding box clips it (there is a nearly negligible speed penalty in doing this). When false, the bounding box clips the surface, exposing the back sides of facets. Setting this to false can be useful with OpenSCAD's "View > Thrown together" menu option to distinguish inside from outside. Default: true +// show_stats = If true, display statistics about the metaball isosurface in the console window. Besides the number of voxels found to contain the surface, and the number of triangles making up the surface, this is useful for getting information about a smaller bounding box possible, to improve speed for subsequent renders. Enabling this parameter has a speed penalty. Default: false +// Example(3D): A group of five spherical metaballs with different charges. The parameter `show_stats=true` (not shown here) was used to find a compact bounding box for this figure. +// centers = [[-20,-20,-20], [-0,-20,-20], +// [0,0,0], [0,0,20], [20,20,10] ]; +// charges = [5, 4, 3, 5, 7]; +// type = MB_SPHERE; +// isovalue = 1; +// voxelsize = 1.5; +// boundingbox = [[-30,-31,-31], [32,31,31]]; +// metaballs(voxelsize, boundingbox, isovalue=isovalue, +// ball_centers=centers, charge=charges, ball_type=type); +// Example(3D,NoAxes): A metaball can have negative charge. In this case we have two metaballs in close proximity, with the small negative metaball creating a dent in the large positive one. The large metaball is shown transparent with small spheres at the center of each. The small one isn't visible because its field is negative; the isosurface encloses only field values greater than the isovalue of 1. +// centers = [[-1,0,0], [1.25,0,0]]; +// charges = [8, -3]; +// type = MB_SPHERE; +// voxelsize = 0.25; +// isovalue = 1; +// boundingbox = [[-7,-6,-6], [3,6,6]]; +// +// #metaballs(voxelsize, boundingbox, isovalue=isovalue, +// ball_centers=centers, charge=charges, ball_type=type); +// color("green") for(c=centers) translate(c) sphere(d=1, $fn=16); +// Example(3D,NoAxes): A cube, a rounded cube, and an octahedron interacting. +// centers = [[-7,-3,27], [7,5,21], [10,0,10]]; +// charge = 5; +// type = [MB_CUBE, MB_ROUNDCUBE, MB_OCTAHEDRON]; +// voxelsize = 0.4; // a bit slow at this resolution +// isovalue = 1; +// boundingbox = [[-13,-9,3], [16,11,33]]; +// +// metaballs(voxelsize, boundingbox, isovalue=isovalue, +// ball_centers=centers, charge=charge, ball_type=type); +// Example(3D,NoAxes): Interaction between two torus-shaped fields in different orientations. +// centers = [[-10,0,17], [7,6,21]]; +// charges = [[6,2], [7,3]]; +// type = MB_TORUS; +// axis_orient = [[0,0,1], [0,1,0]]; +// voxelsize = 0.5; +// isovalue = 1; +// boundingbox = [[-19,-9,9], [18,10,32]]; +// +// metaballs(voxelsize, boundingbox, isovalue=isovalue, ball_centers=centers, +// charge=charges, ball_type=type, additional=axis_orient); +// Example(3D): Demonstration of including a custom metaball function, in this case a sphere with some noise added to its electric field. +// noisy_sphere = function (cdist, charge, additional, +// rotation_matrix_unused, rcutoff=INF) +// let( +// r = norm(cdist) + rands(0, 0.2, 1)[0], +// suppress = let(a=min(r,rcutoff)/rcutoff) 1-a*a +// ) r==0 ? 1000*charge : suppress*charge / r; +// +// centers = [[-9,0,0], [9,0,0]]; +// charge = 5; +// type = [MB_SPHERE, MB_CUSTOM]; +// fieldfuncs = [undef, noisy_sphere]; +// voxelsize = 0.4; +// boundingbox = [[-16,-8,-8], [16,8,8]]; +// +// metaballs(voxelsize, boundingbox, isovalue=1, +// ball_centers=centers, charge=charge, ball_type=type, +// field_function=fieldfuncs); +// Example(3D,NoAxes): A complex example using ellipsoids, spheres, and a torus to make a tetrahedral object with rounded feet and a ring on top. The bottoms of the feet are flattened by limiting the minimum z value of the bounding box. The center of the object is thick due to the contributions of four ellipsoids converging. Designing an object like this using metaballs requires trial and error with low-resolution renders. +// ztheta = 90-acos(-1/3); +// cz = cos(ztheta); +// sz = sin(ztheta); +// type = [ +// MB_ELLIPSOID, MB_ELLIPSOID, +// MB_ELLIPSOID, MB_ELLIPSOID, +// MB_TORUS, MB_SPHERE, MB_SPHERE, MB_SPHERE +// ]; +// centers = [ +// [0,0,20], [20*cz,0,20*sz], +// zrot(120, p=[20*cz,0,20*sz]), +// zrot(-120, p=[20*cz,0,20*sz]), +// [0,0,35], [32*cz,0,32*sz], +// zrot(120, p=[32*cz,0,32*sz]), +// zrot(-120, p=[32*cz,0,32*sz])]; +// cutoff = 40; // extent of influence of each ball +// rotation = [ +// [0,90,0], [0,-ztheta,0], +// [0,-ztheta,120], [0,-ztheta,-120], +// [0,0,0], undef, undef, undef]; +// axis = [ +// undef, undef, undef, undef, +// [0,1,0], undef, undef, undef +// ]; +// charge = [ +// [6,2,2], [7,2,2], [7,2,2], [7,2,2], +// [8,2], 5, 5, 5 +// ]; +// +// voxelsize = 1; +// isovalue = 1; +// boundingbox = [[-23,-36,-15], [39,36,46]]; +// +// // useful to save as VNF for copies and manipulations +// vnf = metaballs(voxelsize, boundingbox, isovalue=isovalue, +// ball_centers=centers, charge=charge, ball_type=type, +// additional=axis, rotation=rotation, radial_cutoff=cutoff); +// vnf_polyhedron(vnf); + +module metaballs(voxel_size, bounding_box, isovalue, ball_centers, charge=10, ball_type=MB_SPHERE, rotation=undef, field_function=undef, additional=undef, radial_cutoff=INF, close_clip=true, show_stats=false) { + vnf = metaballs(voxel_size, bounding_box, isovalue, ball_centers, charge, ball_type, rotation, field_function, additional, radial_cutoff, close_clip, show_stats); + vnf_polyhedron(vnf); +} + +function metaballs(voxel_size, bounding_box, isovalue, ball_centers, charge=10, ball_type=MB_SPHERE, rotation=undef, field_function=undef, additional=undef, radial_cutoff=INF, close_clip=true, show_stats=false) = let( + isoval = is_vector(isovalue) ? isovalue[0] : isovalue, + nballs = len(ball_centers), + chg = is_list(charge) ? charge : repeat(charge, nballs), + interact = is_list(ball_type) ? ball_type : repeat(ball_type, nballs), + rotations = is_list(rotation) ? rotation : repeat(rotation, nballs), + fieldfuncs = is_list(field_function) ? field_function : repeat(field_function, nballs), + addl0 = is_list(additional) ? additional : repeat(additional, nballs), + rlimit = is_list(radial_cutoff) ? radial_cutoff : repeat(radial_cutoff, nballs) +) + assert(all_defined([voxel_size, bounding_box, isovalue, ball_centers]), "\nThe parameters voxel_size, bounding_box, isovalue, and ball centers must all be defined.") + assert(is_list(ball_centers), "\nball_centers must be a list of [x,y,z] coordinates; for a single value use [[x,y,z]].") + assert(len(chg)==nballs, "\nThe list of charges must be equal in length to the list of ball_centers.") + assert(len(interact)==nballs, "\nThe list of ball_types must be equal in length to the list of ball centers.") + assert(len(rotations)==nballs, "\nThe list of rotation vectors must be equal in length to the list of ball centers.") + assert(len(fieldfuncs)==nballs, "\nThe list of field_functions must be equal in length to the list of ball centers.") + assert(len(addl0)==nballs, "\nThe list of additional field function parameters must be equal in length to the list of ball centers.") + assert(len(rlimit)==nballs, "\nThe radial_cutoff list must be equal in length to the list of ball_centers.") +let( + dum_align = _metaball_errchecks(nballs, interact, chg, addl0, fieldfuncs), + + // change MB_ROUNDCUBE squareness to exponents + addl = [ + for(i=[0:nballs-1]) + if (interact[i]==MB_ROUNDCUBE) + _squircle_se_exponent(addl0[i]==undef ? 0.5 : addl0[i]) + else if (interact[i]==MB_TORUS) + addl0[i]==undef ? [0,0,1] : addl0[i] + else + addl0[i] + ], + + // set up rotation matrices in advance + rotmatrix = [ + for(i=[0:nballs-1]) + rotations[i]==undef ? rot([0,0,0]) : rot(rotations[i]) + ], + + //set up function call array + funcs = [ + _metaball_sphere, //MB_SPHERE + _metaball_ellipsoid, //MB_ELLIPSOID + _metaball_roundcube, //MB_ROUNDCUBE + _metaball_cube, //MB_CUBE + _metaball_octahedron, //MB_OCTAHEDRON + _metaball_torus, //MB_TORUS + fieldfuncs //MB_CUSTOM + ], + + // set up field array + v0 = bounding_box[0], + b1 = bounding_box[1], + halfvox = 0.5*voxel_size, + fieldarray = [ + for(x=[v0[0]:voxel_size:b1[0]+halfvox]) [ + for(y=[v0[1]:voxel_size:b1[1]+halfvox]) [ + for(z=[v0[2]:voxel_size:b1[2]+halfvox]) + _metaball_fieldfunc([x,y,z], nballs, ball_centers, chg, interact, rotmatrix, addl, rlimit, funcs) + ] + ] + ] +) isosurface_array(voxel_size, isovalue, fieldarray, origin=v0, close_clip=close_clip, show_stats=show_stats); + + +function _metaball_errchecks(nballs, interact, charge, addl0, fieldfuncs) = [ +for(i=[0:nballs-1]) let( + dumm0 = assert(interact[i] != MB_ELLIPSOID || (interact[i]==MB_ELLIPSOID && is_vector(charge[i]) && len(charge[i])==3), "\nThe MB_ELLIPSOID charge value must be a vector of three numbers.") 0, + dumm1 = assert(interact[i] != MB_ROUNDCUBE || (interact[i]==MB_ROUNDCUBE && (is_undef(addl0[i]) || (is_num(addl0[i]) && 0<=addl0[i] && addl0[i]<=1))), "\nFor MB_ROUNDCUBE, additional parameter must be undef or a single number between 0.0 and 1.0.") 0, + dumm2 = assert(interact[i] != MB_TORUS || (interact[i]==MB_TORUS && is_vector(charge[i]) && len(charge[i])==2), "\nThe MB_TORUS charge value must be a vector of two numbers representing major and minor charges.") 0, + dumm4 = assert(interact[i] != MB_TORUS || (interact[i]==MB_TORUS && (addl0[i]==undef || (norm(addl0[i])==1 && sum(addl0[i])==1))), str("\nMB_TORUS ", i, " additional parameters (", addl0[i], ") must be a unit vector in the x, y, or z direction only.")) 0, + dumm5 = assert(interact[i] != MB_CUSTOM || (interact[i]==MB_CUSTOM && is_def(fieldfuncs[i])), "\nThe MB_CUSTOM ball_type requires a field_function to be defined.") 0 + ) 0 +]; From 0100a51e35e17b8afb3f41aa5c3f1a29b64021d2 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Wed, 22 Jan 2025 23:03:29 -0800 Subject: [PATCH 5/7] Isosurfaces and metaballs, new features (corrected for 2021.01) --- isosurface.scad | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/isosurface.scad b/isosurface.scad index 57b589f2..86afc9fa 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -691,7 +691,7 @@ _MCTriangleTable_reverse = [ // // bbox = [[-100,-100,-100], [100,100,100]]; // isosurface(voxel_size=5, bounding_box=bbox, isovalue=0, -// gyroid, additional=200, close_clip=false); +// field_function=gyroid, additional=200, close_clip=false); // Example(3D,NoAxes): If we remove the `close_clip` parameter or set it to true, the isosurface algorithm encloses the entire half-space bounded by the "inner" gyroid surface, leaving only the "outer" surface exposed. This is a manifold shape but not what we want if trying to model a gyroid. // gyroid = function (xyz, wavelength) let( // p = 360/wavelength, @@ -702,7 +702,7 @@ _MCTriangleTable_reverse = [ // // bbox = [[-100,-100,-100], [100,100,100]]; // isosurface(voxel_size=5, bounding_box=bbox, isovalue=0, -// gyroid, additional=200); +// field_function=gyroid, additional=200); // Example(3D,ThrownTogether,NoAxes): To make the gyroid a double-sided surface, we need to specify a small range around zero for `isovalue`. Now we have a double-sided surface although with `clip_close=false` the edges are not closed where the surface is clipped by the bounding box. // gyroid = function (xyz, wavelength) let( // p = 360/wavelength, @@ -713,7 +713,7 @@ _MCTriangleTable_reverse = [ // // bbox = [[-100,-100,-100], [100,100,100]]; // isosurface(voxel_size=5, bounding_box=bbox, isovalue=[-0.3, 0.3], -// gyroid, additional=200, close_clip=false); +// field_function=gyroid, additional=200, close_clip=false); // Example(3D,ThrownTogether,NoAxes): To make the gyroid a valid manifold 3D object, we remove the `close_clip` parameter (same as setting `close_clip=true`), which closes the edges where the surface is clipped by the bounding box. The resulting object can be tiled, the VNF returned by the functional version can be wrapped around an axis using `{{vnf_bend()}}`, and other operations. // gyroid = function (xyz, wavelength) let( // p = 360/wavelength, @@ -724,7 +724,7 @@ _MCTriangleTable_reverse = [ // // bbox = [[-100,-100,-100], [100,100,100]]; // isosurface(voxel_size=5, bounding_box=bbox, isovalue=[-0.3, 0.3], -// gyroid, additional=200); +// field_function=gyroid, additional=200); // Example(3D,NoAxes): An approximation of the triply-periodic minimal surface known as [Schwartz P](https://en.wikipedia.org/wiki/Schwarz_minimal_surface). // schwartz_p = function (xyz, wavelength) let( // p = 360/wavelength, @@ -735,7 +735,7 @@ _MCTriangleTable_reverse = [ // // bbox = [[-100,-100,-100], [100,100,100]]; // isosurface(voxel_size=4, bounding_box=bbox, isovalue=[-0.2,0.2], -// schwartz_p, additional=100); +// field_function=schwartz_p, additional=100); // Example(3D,NoAxes): Another approximation of the triply-periodic minimal surface known as [Neovius](https://en.wikipedia.org/wiki/Neovius_surface). // neovius = function (xyz, wavelength) let( // p = 360/wavelength, @@ -746,7 +746,7 @@ _MCTriangleTable_reverse = [ // // bbox = [[-100,-100,-100], [100,100,100]]; // isosurface(voxel_size=4, bounding_box=bbox, isovalue=[-0.3,0.3], -// neovius, additional=200); +// field_function=neovius, additional=200); module isosurface(voxel_size, bounding_box, isovalue, field_function, additional, reverse=false, close_clip=true, show_stats=false) { vnf = isosurface(voxel_size, bounding_box, isovalue, field_function, additional, reverse, close_clip, show_stats); From 50b8090c3fc44b124479787a975fa1bce2a01e53 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Wed, 22 Jan 2025 23:33:41 -0800 Subject: [PATCH 6/7] Replaced tabs with spaces --- isosurface.scad | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/isosurface.scad b/isosurface.scad index 86afc9fa..1f980dee 100644 --- a/isosurface.scad +++ b/isosurface.scad @@ -96,18 +96,18 @@ function _bbox_faces(v0, voxsize, bbox) = let( /// Pair of vertex indices for each edge on the voxel _MCEdgeVertexIndices = [ - [0, 1], - [1, 3], - [3, 2], - [2, 0], - [4, 5], - [5, 7], - [7, 6], - [6, 4], - [0, 4], - [1, 5], - [3, 7], - [2, 6], + [0, 1], + [1, 3], + [3, 2], + [2, 0], + [4, 5], + [5, 7], + [7, 6], + [6, 4], + [0, 4], + [1, 5], + [3, 7], + [2, 6], ]; /// For each of the 255 configurations of a marching cube, define a list of triangles, specified as triples of edge indices. From c67b7b5694fbc5f8ba714448f947ceec4ba69400 Mon Sep 17 00:00:00 2001 From: Alex Matulich Date: Thu, 23 Jan 2025 11:54:16 -0800 Subject: [PATCH 7/7] Fix #1547 - typos in example, minor spelling and grammar improvements --- tutorials/Beziers_for_Beginners.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tutorials/Beziers_for_Beginners.md b/tutorials/Beziers_for_Beginners.md index 7e6434fc..93e4119d 100755 --- a/tutorials/Beziers_for_Beginners.md +++ b/tutorials/Beziers_for_Beginners.md @@ -1,6 +1,6 @@ # Béziers for Beginners -Bézier curves are parametric curves defined by polynomial equations. To work with Béziers in OpenSCAD we need to load the Bézier extension BOSL2/beziers.scad in addition to BOSL2/std.scad. +Bézier curves are parametric curves defined by polynomial equations. To work with Béziers in OpenSCAD we need to load BOSL2/std.scad, which includes the extension beziers.scad. Bézier curves vary by the degree of the polynomial that defines the curve. @@ -49,7 +49,7 @@ debug_bezier(bez, N = 3); For a live example of cubic Béziers see the [Desmos Graphing Calculator](https://www.desmos.com/calculator/cahqdxeshd). -Higher order Béziers such as Quartic (degree 4) and Quintic (degree 5) Béziers exist as well. Degree 4 Béziers are used by [round_corners()](https://github.com/BelfrySCAD/BOSL2/wiki/rounding.scad#function-round_corners) and in the continuous rounding operations of [rounded_prism()](https://github.com/BelfrySCAD/BOSL2/wiki/rounding.scad#functionmodule-rounded_prism). +Higher order Béziers such as quartic (degree 4) and quintic (degree 5) Béziers exist as well. Degree 4 Béziers are used by [round_corners()](https://github.com/BelfrySCAD/BOSL2/wiki/rounding.scad#function-round_corners) and in the continuous rounding operations of [rounded_prism()](https://github.com/BelfrySCAD/BOSL2/wiki/rounding.scad#functionmodule-rounded_prism). ![Image courtesy Wikipedia](images/bezier_4_big.gif "Quartic Bézier Animation courtesy Wikipedia") @@ -81,9 +81,9 @@ debug_bezier(bez, N = 3); ## Bézier Paths -A Bézier path is when we string together a sequence of Béiers with coincident endpoints. +A Bézier path is when we string together a sequence of Béziers with coincident endpoints. -The point counts arise as a natural consequence of what a bezier path is. If you have k beziers of order N then that's k(N+1) points, except we have k-1 overlaps, so instead it's +The point counts arise as a natural consequence of what a Bézier path is. If you have k Béziers of order N, then that's k(N+1) points, except we have k-1 overlaps, so instead it's ```math k(N+1)-(k-1) = kN +k -k+1 = kN+1. @@ -94,7 +94,7 @@ The list of control points for a Bézier is not an OpenSCAD path. If we treat th ```openscad-2D include -bez = [[0,0], [30,30], [0,50], {70,30] [0,100]]; +bez = [[0,0], [30,30], [0,50], [70,30], [0,100]]; debug_bezier(bez, N = 2); @@ -120,7 +120,7 @@ stroke(path); Bézier paths can be made up of more than one Bézier curve. Quadratic Bezier paths have a multiple of 2 points plus 1, and cubic Bézier paths have a multiple of 3 points plus 1 -This means that a series of 7 control points can be grouped into three (overlapping) sets of 3 and treated as a sequence of 3 quadratic beziers. The same 7 points can be grouped into two overlapping sets of 4 and treated as a sequence of two cubic beziers. The two paths have significantly different shapes. +This means that a series of 7 control points can be grouped into three (overlapping) sets of 3 and treated as a sequence of 3 quadratic Béziers. The same 7 points can be grouped into two overlapping sets of 4 and treated as a sequence of two cubic Béziers. The two paths have significantly different shapes. ```openscad-2D include @@ -138,7 +138,7 @@ path = bezpath_curve(bez, N=3); //make a cubic Bézier path stroke(path); ``` -By default [bezpath_curve()](https://github.com/BelfrySCAD/BOSL2/wiki/beziers.scad#function-bezpath_curve) takes a Bézier path and converts it to an OpenSCAD path by splitting each Bézier curve into 16 straight-line segments. The segments are not necessarily of equal length. Note that the special variable $fn has no effect on the number of steps. You can control this number using the **splinesteps** argument. +By default [bezpath_curve()](https://github.com/BelfrySCAD/BOSL2/wiki/beziers.scad#function-bezpath_curve) takes a Bézier path and converts it to an OpenSCAD path by splitting each Bézier curve into 16 straight-line segments. The segments are not necessarily of equal length. The special variable $fn has no effect on the number of steps. You can control this number using the **splinesteps** argument. ```openscad-2D include @@ -222,9 +222,9 @@ path = offset_stroke(bezier_curve(bez, splinesteps = 32), [2,0]); back_half(s = 200) rotate_sweep(path,360); ``` -We'll use a cylinder with a height of 2 for the floor of our vase. At the bottom of the vase the radius of the hole is bez[0].x but we need to find the radius at y = 2. The function [bezier_line_intersection()](https://github.com/BelfrySCAD/BOSL2/wiki/beziers.scad#function-bezier_line_intersection) returns a list of u-values where a given line intersects our Bézier curve. +We use a cylinder with a height of 2 for the floor of our vase. At the bottom of the vase the radius of the hole is bez[0].x but we need to find the radius at y = 2. The function [bezier_line_intersection()](https://github.com/BelfrySCAD/BOSL2/wiki/beziers.scad#function-bezier_line_intersection) returns a list of u-values where a given line intersects our Bézier curve. -The u-value is a number between 0 and 1 that designates how far along the curve the intersections occur. In our case the line only crosses the Bézier at one point so we get the single-element list [0.0168783]. +The u-value is a number between 0 and 1 that designates how far along the curve the intersections occur. In our case the line crosses the Bézier only at one point so we get the single-element list [0.0168783]. The function [bezier_points()](https://github.com/BelfrySCAD/BOSL2/wiki/beziers.scad#function-bezpath_points) converts that list of u-values to a list of x,y coordinates. Drawing a line at y = 2 gives us the single-element list [[17.1687, 2]]. @@ -281,7 +281,7 @@ BOSL2 includes four functions for constructing Cubic Bézier paths: [bez_begin()](https://github.com/BelfrySCAD/BOSL2/wiki/beziers.scad#function-bez_begin) and [bez_end()](https://github.com/BelfrySCAD/BOSL2/wiki/beziers.scad#function-bez_end) define the endpoints of a simple cubic Bézier curve. -Because each constructor function produces a list of points , we'll use the [flatten()](https://github.com/BelfrySCAD/BOSL2/wiki/lists.scad#function-flatten) function to consolidate them into a single list. +Because each constructor function produces a list of points , we use the [flatten()](https://github.com/BelfrySCAD/BOSL2/wiki/lists.scad#function-flatten) function to consolidate them into a single list. There are three different ways to specify the location of the endpoints and control points. @@ -332,7 +332,7 @@ bez = flatten([ debug_bezier(bez,N=3); ``` -The fourth cubic Bézier path constructor is [bez_tang()](https://github.com/BelfrySCAD/BOSL2/wiki/beziers.scad#function-bez_tang). This constructor makes smooth joint. It also has three control points, one on the path and the approaching and departing control points. Because all three points lie on a single line, we need only specify the angle of the departing control point. As in this example you can specify different distances for the approaching and departing controls points. If you specify only a single distance, it is used for both. +The fourth cubic Bézier path constructor is [bez_tang()](https://github.com/BelfrySCAD/BOSL2/wiki/beziers.scad#function-bez_tang). This constructor makes smooth joint. It also has three control points, one on the path and the approaching and departing control points. Because all three points lie on a single line, we need to specify only the angle of the departing control point. As in this example you can specify different distances for the approaching and departing controls points. If you specify only a single distance, it is used for both. We can add a smooth joint to the last example: @@ -391,7 +391,7 @@ bez = flatten([ debug_bezier(bez, N=3); ``` -Similarly, for the heart-shaped path we'll replace a corner point with the start and end points: +Similarly, for the heart-shaped path we replace a corner point with the start and end points: ```openscad-2D include @@ -511,7 +511,7 @@ debug_bezier(bez, N=3); ### A Bud Vase Design using both 2D and 3D Bézier Paths -We can use a 2D Bézier path to define the shape of our bud vase as we did in the examples above. Instead of using a [rotate_sweep()](https://github.com/BelfrySCAD/BOSL2/wiki/skin.scad#functionmodule-rotate_sweep) to make a vase with a circular cross section we'll use a 3D Bèzier path that both defines the cross section and makes the top more interesting. This design uses the [skin()](https://github.com/BelfrySCAD/BOSL2/wiki/skin.scad#functionmodule-skin) module to create the final geometry. +We can use a 2D Bézier path to define the shape of our bud vase as we did in the examples above. Instead of using a [rotate_sweep()](https://github.com/BelfrySCAD/BOSL2/wiki/skin.scad#functionmodule-rotate_sweep) to make a vase with a circular cross section we use a 3D Bèzier path that both defines the cross section and makes the top more interesting. This design uses the [skin()](https://github.com/BelfrySCAD/BOSL2/wiki/skin.scad#functionmodule-skin) module to create the final geometry. ```openscad-3d,Big include