/// It is more efficient to have this static table than to call reverse() repeatedly while triangulating (although this static table was generated that way).
/// In keeping with the convention for triangulating an isosurface through a voxel, analogous to the case in which two surfaces separate two diagonally opposite high-value corners of one face, in 2D contour terms it is assumed there is a valley separating two high corners, not a ridge connecting them. The 8 ambiguous triangulation cases for opposing corners are set up accordingly. These are the rotational groups of indices {10,30}, {11,19,33,57}, {20,60} in the array below.
/// For each of the 81 possible configurations of a clip face intersected by a minimum and/or maximum isovalue, define a list of triangles, specified as pairs of corner ID and edge ID arrays, with a total of 3 points in each pair. Each pair has the form [corner],[edge1,edge2] or [corner1,corner2],[edge], or [corner1,corner2,corner3],[] or [],[edge1,edge2,edge3].
// "(xRotations)" for number of rotation versions, or
// "(Rotation n from decimal index)" indicating which decimal index this was rotated from, where n=the number of 90° clockwise rotations from the original.
/// 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.
/// 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.
/// Built-in metaball functions corresponding to each MB_ index.
/// For speed, they are split into four functions, each handling a different combination of influence != 1 or influence == 1, and cutoff < INF or cutoff == INF.
// * A function literal accepting a 3-vector representing a point in space relative to the metaball's center.
// * An array containing a function literal and a debug VNF, as `[custom_func, [sign, vnf]]`, where `sign` is the sign of the metaball and `vnf` is the VNF to show in the debug view when `debug=true` is set.
// The built-in metaball functions are listed below. As usual, arguments without a trailing `=` can be used positionally; arguments with a trailing `=` must be used as named arguments.
// * `mb_sphere(r|d=)` — spherical metaball, with radius `r` or diameter `d`. You can create an ellipsoid using `scale()` as the last transformation entry of the metaball `spec` array.
// * `mb_cuboid(size, [squareness=])` — cuboid metaball with rounded edges and corners. The corner sharpness is controlled by the `squareness` parameter ranging from 0 (spherical) to 1 (cubical), and defaults to 0.5. The `size` parameter specifies the dimensions of the cuboid that circumscribes the rounded shape, which is tangent to the center of each cube face. The `size` parameter may be a scalar or a vector, as in {{cuboid()}}. Except when `squareness=1`, the faces are always a little bit curved.
// * `mb_cyl(h|l|height|length, [r|d=], [r1=|d1=], [r2=|d2=], [rounding=])` — vertical cylinder or cone metaball with the same dimensional arguments as {{cyl()}}. At least one of the radius or diameter arguments is required. The `rounding` argument defaults to 0 (sharp edge) if not specified. Only one rounding value is allowed: the rounding is the same at both ends. For a fully rounded cylindrical shape, consider using `mb_disk()` or `mb_capsule()`, which are less flexible but have faster execution times.
// * `mb_disk(h|l|height|length, r|d=)` — flat disk with rounded edge, using the same dimensional arguments as {{cyl()}}. The diameter specifies the total diameter of the shape including the rounded sides, and must be greater than its height.
// * `mb_capsule(h|l|height|length, [r|d=]` — vertical cylinder with rounded caps, using the same dimensional arguments as {{cyl()}}. The object resembles a convex hull of two spheres. The height or length specifies the distance between the ends of the hemispherical caps.
// * `mb_connector(p1, p2, [r|d=])` — a connecting rod of radius `r` or diameter `d` with hemispherical caps (like `mb_capsule()`), but specified to connect point `p1` to point `p2` (which must be different 3D coordinates). As with `mb_capsule()`, the object resembles a convex hull of two spheres. The points `p1` and `p2` are at the centers of the two round caps. The connectors themselves are still influenced by other metaballs, but it may be undesirable to have them influence others, or each other. If two connectors are connected, the joint may appear swollen unless `influence` or `cutoff` is reduced. Reducing `cutoff` is preferable if feasible, because reducing `influence` can produce interpolation artifacts.
// * `mb_torus([r_maj|d_maj=], [r_min|d_min=], [or=|od=], [ir=|id=])` — torus metaball oriented perpendicular to the z axis. You can specify the torus dimensions using the same arguments as {{torus()}}; that is, major radius (or diameter) with `r_maj` or `d_maj`, and minor radius and diameter using `r_min` or `d_min`. Alternatively you can give the inner radius or diameter with `ir` or `id` and the outer radius or diameter with `or` or `od`. You must provide a combination of inputs that completely specifies the torus. If `cutoff` is applied, it is measured from the circle represented by `r_min=0`.
// * `mb_octahedron(size, [squareness=])` — octahedron metaball with rounded edges and corners. The corner sharpness is controlled by the `squareness` parameter ranging from 0 (spherical) to 1 (sharp), and defaults to 0.5. The `size` parameter specifies the tip-to-tip distance of the octahedron that circumscribes the rounded shape, which is tangent to the center of each octahedron face. The `size` parameter may be a scalar or a vector, as in {{octahedron()}}. At `squareness=0`, the shape reduces to a sphere curcumscribed by the octahedron. Except when `squareness=1`, the faces are always curved.
// * `cutoff` — positive value giving the distance beyond which the metaball does not interact with other balls. Cutoff is measured from the object's center. Default: INF
// * `hide_debug` — when true, suppresses the display of the underlying metaball shape when `debug=true` is set in the `metaballs()` module. This is useful to hide shapes that may be overlapping others in the debug view. Default: false
// You can pass a custom function as a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals)
// spec = Metaball specification in the form `[trans0, spec0, trans1, spec1, ...]`, with alternating transformation matrices and metaball specs, where `spec0`, `spec1`, etc. can be a metaball function or another metaball specification. See above for more details, and see Example 24 for a demonstration.
// bounding_box = The volume in which to perform computations, expressed as a scalar size of a cube centered on the origin, or a pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]` specifying the minimum and maximum box corner coordinates. Unless you set `exact_bounds=true`, the bounding box size may be enlarged to fit whole voxels.
// voxel_size = Size of the voxels used to sample the bounding box volume, can be a scalar or 3-vector, or omitted if `voxel_count` is set. You may get a non-cubical voxels of a slightly different size than requested if `exact_bounds=true`.
// voxel_count = Approximate number of voxels in the bounding box. If `exact_bounds=true` then the voxels may not be cubes. Use with `show_stats=true` to see the corresponding voxel size. Default: 10000 (if `voxel_size` not set)
// isovalue = A scalar value specifying the isosurface value (threshold value) of the metaballs. At the default value of 1.0, the internal metaball functions are designd so the size arguments correspond to the size parameter (such as radius) of the metaball, when rendered in isolation with no other metaballs. Default: 1.0
// closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face, possibly producing non-manfold metaballs with holes where the bounding box intersects them. Default: true
// exact_bounds = When true, shrinks voxels as needed to fit whole voxels inside the requested bounding box. When false, enlarges `bounding_box` as needed to fit whole voxels of `voxel_size`, and centers the new bounding box over the requested box. Default: false
// show_stats = If true, display statistics about the metaball isosurface in the console window. Besides the number of voxels that the surface passes through, and the number of triangles making up the surface, this is useful for getting information about a possibly smaller bounding box to improve speed for subsequent renders. Enabling this parameter has a small speed penalty. Default: false
// convexity = (Module only) Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6
// show_box = (Module only) Display the requested bounding box as transparent. This box may appear slightly inside the bounds of the figure if the actual bounding box had to be expanded to accommodate whole voxels. Default: false
// debug = (Module only) Display the underlying primitive metaball shapes using your specified dimensional arguments, overlaid by the transparent metaball scene. Positive metaballs appear blue, negative appears orange, and any custom function with no debug VNF defined appears as a gray tetrahedron of corner radius 5.
// cp = (Module only) Center point 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 = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// orient = (Module only) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
// Example(3D,NoAxes,VPR=[75,0,20]): Two octahedrons interacting. Here `voxel_size` is not given, so it defaults to a value that results in approximately 10,000 voxels in the bounding box. Adding the parameter `show_stats=true` displays the voxel size used, along with other information.
// Example(3D,VPD=110): These next five examples demonstrate the different types of metaball interactions. We start with two spheres 30 units apart. Each would have a radius of 10 in isolation, but because they are influencing their surroundings, each sphere mutually contributes to the size of the other. The sum of contributions between the spheres add up so that a surface plotted around the region exceeding the threshold defined by `isovalue=1` looks like a peanut shape surrounding the two spheres.
// Example(3D,VPD=110): Adding a cutoff of 25 to the left sphere causes its influence to disappear completely 25 units away (5 units from the center of the right sphere). The left sphere is bigger because it still receives the full influence of the right sphere, but the right sphere is smaller because the left sphere has no contribution past 25 units. The right sphere is not abruptly cut off because the cutoff function is smooth and influence is normal. Setting cutoff too small can remove the interactions of one metaball from all other metaballs, leaving that metaball alone by itself.
// Example(3D,VPD=110): Here, the left sphere has less influence in addition to a cutoff. Setting `influence=0.5` results in a steeper falloff of contribution from the left sphere. Each sphere has a different size and shape due to unequal contributions based on distance.
// Example(3D,VPD=110): In this example, we have two size-10 spheres as before and one tiny sphere of 1.5 units radius offset a bit on the y axis. With an isovalue of 1, this figure would appear similar to Example 9 above, but here the isovalue has been set to 2, causing the surface to shrink around a smaller volume values greater than 2. Remember, higher isovalue thresholds cause metaballs to shrink.
// Example(3D,VPD=110): Keeping `isovalue=2`, the influence of the tiny sphere has been set quite high, to 10. Notice that the tiny sphere shrinks a bit, but it has dramatically increased its contribution to its surroundings, causing the two other spheres to grow and meld into each other. The `influence` argument on a small metaball affects its surroundings more than itself.
// Example(3D,Med): Setting `influence` to less than 0.5 can cause interpolation artifacts in the surface. The only difference between these two spheres is `influence`. Both have `cutoff` set to prevent them from affecting each other. The sphere on the right has a low influence of 0.02, which translates to a falloff with distance $d$ proportional to $1/d^{50}$. That high exponent increases the *non-linear* nature of the function gradient at the isosurface, reducing the accuracy of the *linear* interpolation of where the the surface intersects each voxel, causing ridges to appear. You could use this to create a texture deliberately, but it is usually better to use `cutoff` to limit the range of influence rather than reducing `influence` significantly below 1.
// Example(3D,NoAxes): A group of five spherical metaballs with different sizes. The parameter `show_stats=true` (not shown here) was used to find a compact bounding box for this figure. Here instead of setting `voxel_size`, we set `voxel_count` for approximate number of voxels in the bounding box, and the voxel size is adjusted to fit. Setting `exact_bounds=true` forces the bounding box to be fixed, and a non-cubic voxel is then used to fit within that box.
// Example(3D,NoAxes): A metaball can be negative. In this case we have two metaballs in close proximity, with the small negative metaball creating a dent in the large positive one. The positive metaball is shown transparent, and small spheres show the center of each metaball. The negative metaball isn't visible because its field is negative; the isosurface encloses only field values greater than the isovalue of 1.
// Example(3D,VPD=105,VPT=[3,5,4.7]): When a positive and negative metaball interact, the negative metaball reduces the influence of the positive one, causing it to shrink, but not disappear because its contribution approaches infinity at its center. In this example we have a large positive metaball near a small negative metaball at the origin. The negative ball has high influence, and a cutoff limiting its influence to 20 units. The negative metaball influences the positive one up to the cutoff, causing the positive metaball to appear smaller inside the cutoff range, and appear its normal size outside the cutoff range. The positive metaball has a small dimple at the origin (the center of the negative metaball) because it cannot overcome the infinite negative contribution of the negative metaball at the origin.
// Example(3D,NoAxes,VPD=80,VPT=[3,0,19]): A sharp cube, a rounded cube, and a sharp octahedron interacting. Because the surface is generated through cubical voxels, voxel corners are always cut off, resulting in difficulty resolving some sharp edges.
// Example(3D,NoAxes,VPD=205,Med): A toy airplane, constructed only from metaball spheres with scaling. The bounding box is used to clip the wingtips, tail, and belly of the fuselage.
// Example(3D,VPD=60,VPR=[57,0,50],VPT=[0.5,2,1.8]): Custom metaballs are an advanced technique in which you define your own metaball shape by passing a function literal that takes a single argument: a coordinate in space relative to the metaball center called `point` here, but can be given any name. This distance vector from the origin is calculated internally and always passed to the function. Inside the function, it is converted to a scalar distance `dist`. The function literal expression sets all of your parameters. Only `point` is not set, and it becomes the single parameter to the function literal. The `spec` argument invokes your custom function as a function literal that passes `point` into it.
// Example(3D,VPD=60,VPR=[57,0,50],VPT=[0.5,2,1.8]): Here is a function nearly identical to the previous example, introducing additional dimensional parameters into the function to control its size and number of lobes. The bounding box size here is as small as possible for calculation efficiency, but if you expiriment with this using different argument values, you should increase the bounding box along with voxel size.
// Example(3D): Next we show how to create a function that works like the built-ins. **This is a full implementation** that allows you to specify the function directly by name in the `spec` argument without needing the function literal syntax, and without needing the `point` argument in `spec`, as in the prior examples. Here, `noisy_sphere_calcs() is the calculation function that accepts the `point` position argument and any other parameters needed (here `r` and `noise_level`), and returns a single value. Then there is a "master" function `noisy_sphere() that does some error checking and returns an array consisting of (a) a function literal expression that sets all of your parameters, and (b) another array containing the metaball sign and a simple "debug" VNF representation of the metaball for viewing when `debug=true` is passed to `metaballs()`. The call to `mb_cutoff()` at the end handles the cutoff function for the noisy ball consistent with the other internal metaball functions; it requires `dist` and `cutoff` as arguments. You are not required to use this implementation in your own custom functions; in fact it's easier simply to declare the function literal in your `spec` argument, but this example shows how to do it all.
// Example(3D,Med,NoAxes,VPR=[55,0,0],VPD=200,VPT=[7,2,2]): Demonstration of `debug=true` with a more complex example using ellipsoids, a capsule, 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 clipping with the bottom of the bounding box. The center of the object is thick due to the contributions of three ellipsoids and a capsule converging. Designing an object like this using metaballs requires trial and error with low-resolution renders.
// Example(3D,Med,NoAxes,VPR=[70,0,30],VPD=520,VPT=[0,0,80]): This example demonstrates grouping metaballs together and nesting them in lists of other metaballs, to make a crude model of a hand. Here, just one finger is defined, and a thumb is defined from one less joint in the finger. Individual fingers are grouped together with different positions and scaling, along with the thumb. Finally, this group of all fingers is used to combine with a rounded cuboid, with a slight ellipsoid dent subtracted to hollow out the palm, to make the hand.
// Example(3D,NoAxes,Med,VPD=235,VPR=[83,0,320],VPT=[-5,-5,43]): A model of a giraffe using a variety of different metaball shapes. Features such as the tail and lower legs are thin, so a small voxel size is required to render them.
// Example(3D,Med,NoAxes,VPD=228,VPT=[1,-5,35]): A model of a bunny, assembled from separate body components made with metaballs, with each component rendered at a different voxel size, and then combined together along with eyes and teeth. In this way, smaller bounding boxes can be defined for each component, which speeds up rendering. A bit more time is saved by saving the repeated components (ear, front leg, hind leg) in VNF structures, to render copies with {{vnf_polyhedron()}}.
// Example(3D,Med,NoAxes,VPD=120,VPT=[2,0,6],VPR=[60,0,320]): Specifying `debug=true`, we can see the elements used to construct the duck. Positive metaballs are blue and negative metaballs are orange. Unfortunately, although the head is a rather complex structure, the big blue skull element covers up other details. Note also that removing the voxel_size parameter from `metaballs()` speeds up the preview.
// Example(3D,Med,NoAxes,VPD=79,VPT=[-9,10,10],VPR=[50,0,340]): Adding `hide_debug=true` to the skull metaball function suppresses its display and reveals the neck and cheek components formerly covered by the skull metaball. Here we also disabled the addition of eyeballs, and reduced the size of the bounding box to enclose only the head. The bounding box is for computing the metaball surface; the debug components still display outside these bounds.
// Example(3D,VPD=83,NoAxes): Adapting the multi-lobe function from Example 21 above, here we show how to display a debug-view VNF approximating the shape of the metaball when `debug=true`, *without* resorting to the full custom function implementation demonstrated in Example 22. Rather than having just the function literal in the `spec` array, we use `[function_literal, [sign,vnf]]` instead, where `sign` is the sign of the metaball (-1 or 1) and `vnf` is the VNF of the debug-view shape.
assert(is_undef(voxel_size)||(is_finite(voxel_size)&&voxel_size>0)||(is_vector(voxel_size)&&all_positive(voxel_size)),"\nvoxel_size must be a positive number, a 3-vector of positive values, or not given.")
assert(is_finite(isovalue)||(is_list(isovalue)&&len(isovalue)==2&&is_num(isovalue[0])&&is_num(isovalue[1])),"\nIsovalue must be a number or a range; a number is the same as [number,INF].")
// 2D metaball shapes can be useful to create interesting polygons for extrusion. When invoked as a
// module, a 2D metaball scene is displayed. When called as a function, a [region](regions.scad)
// containing one or more paths is returned.
// .
// For a full explanation of metaballs, see {{metaballs()}} introduction above. The specification
// method, tranformations, and bounding box, and other parameters are the same as in 3D, but in 2D we
// refer to "pixels" rather than "voxels".
// .
// You can create 2D metaballs in a variety of standard shapes using the predefined functions
// listed below. If you wish, you can also create custom metaball shapes using your own functions.
// As with the 3D metaballs, for all of the built-in 2D metaballs, three parameters are available to
// control the interaction of the metaballs with each other: `cutoff`, `influence`, and `negative`.
// These three parameters work the same way as with 3D metaballs.
// .
// ***Built-in 2D metaball functions***
// .
// Several metaballs are defined for you to use in your models.
// All of the built-in metaballs take positional and named parameters that specify the size of the
// metaball (such as height or radius). The size arguments are the same as those for the regular objects
// of the same type (e.g. a sphere accepts both `r` for radius and the named parameter `d=` for
// diameter). The size parameters always specify the size of the metaball **in isolation** with
// `isovalue=1`. The metaballs can grow much bigger than their specified sizes when they interact
// with each other. Changing `isovalue` also changes the sizes of metaballs. They grow bigger than their
// specified sizes, even in isolation, if `isovalue < 1` and smaller than their specified sizes if
// `isovalue > 1`.
// .
// The built-in 2D metaball functions are listed below. As usual, arguments without a trailing `=` can be used positionally; arguments with a trailing `=` must be used as named arguments.
// .
// * `mb_circle(r|d=)` — circular metaball, with radius `r` or diameter `d`. You can create an ellipse using `scale()` as the last transformation entry of the metaball `spec` array.
// * `mb_rect(size, [squareness=])` — a square/circle hybrid known as a squircle, appearing as a square with rounded edges and corners. The corner sharpness is controlled by the `squareness` parameter ranging from 0 (spherical) to 1 (circular), and defaults to 0.5. The `size` parameter specifies the dimensions of the squircle that circumscribes the rounded shape, which is tangent to the center of each square side. The `size` parameter may be a scalar or a vector, as in {{squircle()}}. Except when `squareness=1`, the sides are always a little bit curved.
// * `mb_trapezoid(h, w|w1=, [w2=], [rounding=])` — rounded trapezoid metaball with the same dimensional arguments as {{trapezoid()}}. The `rounding` argument defaults to 0 (sharp edge) if not specified. Only one rounding value is allowed: the rounding is the same at both ends. For a rounded rectangular shape, consider using `mb_rect()`, or `mb_stadium()`, which is less flexible but has faster execution time.
// * `mb_stadium(h|l|height|length, [r|d=]` — vertical cylinder with rounded caps, using similar dimensional arguments as {{cyl()}}. The object resembles a convex hull of two circles. The height or length specifies the distance ends of the circular caps.
// * `mb_connector2d(p1, p2, [r|d=])` — a connecting rod of radius `r` or diameter `d` with circular caps (like `mb_stadium()`), but specified to connect point `p1` to point `p2` (which must be different 2D coordinates). As with `mb_stadium()`, the object resembles a convex hull of two spheres. The points `p1` and `p2` are at the centers of the two round caps. The connectors themselves are still influenced by other metaballs, but it may be undesirable to have them influence others, or each other. If two connectors are connected, the joint may appear swollen unless `influence` or `cutoff` is reduced. Reducing `cutoff` is preferable if feasible, because reducing `influence` can produce interpolation artifacts.
// * `mb_ring(ir|id=, or|od=)` — 2D ring metaball, with inner radius `ir` and outer radius `or`. If `cutoff` is applied, it is measured from the circle midway between `ir` and `or`.
// .
// In addition to the dimensional arguments described above, all of the built-in functions accept the
// following named arguments:
// * `cutoff` — positive value giving the distance beyond which the metaball does not interact with other balls. Cutoff is measured from the object's center. Default: INF
// * `influence` — a positive number specifying the strength of interaction this ball has with other balls. Default: 1
// * `negative` — when true, creates a negative metaball. Default: false
// * `hide_debug` — when true, suppresses the display of the underlying metaball shape when `debug=true` is set in the `metaballs()` module. This is useful to hide shapes that may be overlapping others in the debug view. Default: false
// .
// ***Metaball functions and user defined functions***
// .
// You can construct complicated metaball models using only the built-in metaball functions above.
// However, you can create your own custom metaballs if desired.
// .
// When multiple metaballs are in a model, their functions are summed and compared to the isovalue to
// determine the final shape of the metaball object.
// Each metaball is defined as a function of a 2-vector that gives the value of the metaball function
// for that point in space. As is common in metaball implementations, we define the built-in metaballs using an
// inverse relationship where the metaball functions fall off as $1/d$, where $d$ is distance measured from
// the center or core of the metaball. The spherical metaball therefore has a simple basic definition as
// $f(v) = 1/\text{norm}(v)$. If we choose an isovalue $c$, then the set of points $v$ such that $f(v) >= c$
// defines a bounded set; for example, a sphere with radius depending on the isovalue $c$. The
// default isovalue is $c=1$. Increasing the isovalue shrinks the object, and decreasing the isovalue grows
// the object.
// .
// To adjust interaction strength, the influence parameter applies an exponent, so if `influence=a`
// then the decay becomes $1/d^{1/a}$. This means, for example, that if you set influence to
// 0.5 you get a $1/d^2$ falloff. Changing this exponent changes how the balls interact.
// .
// You can pass a custom function as a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals)
// that takes a 3-vector as its first argument and returns a single numerical value.
// Generally, the function should return a scalar value that drops below the isovalue somewhere within your
// bounding box. If you want your custom metaball function to behave similar to to the built-in functions,
// the return value should fall off with distance as $1/d$.
// .
// ***Debug view***
// .
// The module form of `metaballs2d()` can take a `debug` argument. When you set `debug=true`, the scene is
// rendered as an outline with the primitive metaball shapes shown inside, colored blue for positive,
// orange for negative, or gray for custom metaballs with no sign specified. These shapes are displayed at
// the sizes specified by the dimensional parameters in the corresponding metaball functions, regardless of
// isovalue. Setting `hide_debug=true` in individual metaball functions hides primitive shape from the debug
// view.
// .
// User-defined metaball functions are displayed by default as gray triangles with a corner radius of 5,
// unless you also designate a polygon path for your custom function. To specify a custom polygon for a custom function
// literal, enclose it in square brackets to make a list with the function literal as the first element, and
// another list as the second element, for example:
// `[ function (point) custom_func(point, arg1,...), [sign, path] ]`
// where `sign` is the sign of the metaball and `path` is the path of the polygon to show in the debug view when `debug=true`.
// The sign determines the color of the debug object: `1` is blue, `-1` is orange, and `0` is gray.
// Arguments:
// spec = Metaball specification in the form `[trans0, spec0, trans1, spec1, ...]`, with alternating transformation matrices and metaball specs, where `spec0`, `spec1`, etc. can be a metaball function or another metaball specification.
// bounding_box = The volume in which to perform computations, expressed as a scalar size of a square centered on the origin, or a pair of 2D points `[[xmin,ymin], [xmax,ymax]]` specifying the minimum and maximum box corner coordinates. Unless you set `exact_bounds=true`, the bounding box size may be enlarged to fit whole pixels.
// pixel_size = Size of the pixels used to sample the bounding box area, can be a scalar or 2-vector, or omitted if `pixel_count` is set. You may get a non-square pixels of a slightly different size than requested if `exact_bounds=true`.
// ---
// pixel_count = Approximate number of pixels in the bounding box. If `exact_bounds=true` then the pixels may not be squares. Use with `show_stats=true` to see the corresponding pixel size. Default: 1024 (if `pixel_size` not set)
// isovalue = A scalar value specifying the isosurface value (threshold value) of the metaballs. At the default value of 1.0, the internal metaball functions are designd so the size arguments correspond to the size parameter (such as radius) of the metaball, when rendered in isolation with no other metaballs. Default: 1.0
// closed = When true, close the path if it intersects the bounding box by adding a closing side. When false, do not add a closing side. Default: true
// exact_bounds = When true, shrinks pixels as needed to fit whole pixels inside the requested bounding box. When false, enlarges `bounding_box` as needed to fit whole pixels of `pixel_size`, and centers the new bounding box over the requested box. Default: false
// show_stats = If true, display statistics about the metaball isosurface in the console window. Besides the number of pixels that the contour passes through, and the number of segments making up the contour, this is useful for getting information about a possibly smaller bounding box to improve speed for subsequent renders. Default: false
// show_box = (Module only) Display the requested bounding box as a transparent thin rectangle. This box may appear slightly inside the bounds of the figure if the actual bounding box had to be expanded to accommodate whole pixels. Default: false
// debug = (Module only) Display the underlying primitive metaball shapes using your specified dimensional arguments, overlaid by the transparent metaball scene. Positive metaballs appear blue, negative appears orange, and any custom function with no debug VNF defined appears as a gray tetrahedron of corner radius 5.
// anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// Named Anchors:
// "origin" = Anchor at the origin.
// Example(2D,NoAxes): Two circles interacting.
// spec = [
// left(9), mb_circle(5),
// right(9), mb_circle(5)
// ];
// metaballs2d(spec, pixel_size=1,
// bounding_box=[[-16,-7], [16,7]]);
// Example(2D,NoAxes): Two rounded rectangles (squircles) interacting.
// spec = [
// move([-8,-6]), mb_rect(10),
// move([8,6]), mb_rect(10)
// ];
// metaballs2d(spec, pixel_size=1,
// bounding_box=[[-15,-13], [15,13]]);
// Example(2D,NoAxes): Two rounded trapezoids interacting.
// Example(2D,NoAxes): A circle with two connectors.
// path = [[-20,0], [0,1], [-3,-10]];
// spec = [
// move(path[0]), mb_circle(6),
// for(seg=pair(path)) each
// [IDENT, mb_connector2d(seg[0],seg[1],
// 2, influence=0.5)]
// ];
// metaballs2d(spec, pixel_size=1,
// bounding_box=[[-27,-13], [4,14]]);
// Example(2D,NoAxes): Interaction between two rings.
// spec = [
// move([-7,-3]), mb_ring(ir=3,or=6),
// move([7,3]), mb_ring(ir=3,or=7)
// ];
// pixel_size = 0.5;
// boundingbox = [[-14,-11], [16,11]];
// metaballs2d(spec, boundingbox, pixel_size);
// Example(2D,NoAxes): A positive and negative metaball in close proximity, with the small negative metaball creating a dent in the large positive one. Small green cylinders indicate the center of each metaball. The negative metaball isn't visible because its field is negative; the contour encloses only field values greater than the isovalue of 1.
// Example(2D,VPD=105): When a positive and negative metaball interact, the negative metaball reduces the influence of the positive one, causing it to shrink, but not disappear because its contribution approaches infinity at its center. This example shows a large positive metaball near a small negative metaball at the origin. The negative ball has high influence, and a cutoff limiting its influence to 20 units. The negative metaball influences the positive one up to the cutoff, causing the positive metaball to appear smaller inside the cutoff range, and appear its normal size outside the cutoff range. The positive metaball has a small dimple at the origin (the center of the negative metaball) because it cannot overcome the infinite negative contribution of the negative metaball at the origin.
// spec = [
// back(10), mb_circle(20),
// IDENT, mb_circle(2, influence=30,
// cutoff=20, negative=true),
// ];
// pixel_size = 0.5;
// boundingbox = [[-20,-1], [20,31]];
// metaballs2d(spec, boundingbox, pixel_size);
// Example(2D,NoAxes,VPD=250): Profile of an airplane, constructed only from metaball circles with scaling. The bounding box is used to clip the wingtips and tail.
// bounding_box = [[-55,-50],[35,50]];
// spec = [
// // fuselage
// move([-18,0])*scale([27,4]), mb_circle(1),
// // tail
// move([30,0])*scale([3,15]), mb_circle(1),
// // wing
// move([-15,0])*scale([6,45]), mb_circle(1)
// ];
// pixel_size = 1;
// color("lightblue") zrot(-90)
// metaballs2d(spec, bounding_box, pixel_size);
// Example(2D): This is the 2D version of the 3D Example 20 above, showing a custom metaball defined and passed as a function literal that takes a single [x,y] argument representing a coordinate relative to the metaball center, called `point` here, but can have any name. This distance vector from the origin is calculated internally and always passed to the function. Inside the function, it is converted to a scalar distance `dist`. The function literal expression sets all of your parameters. Only `point` is not set, and it becomes the single parameter to the function literal. The `spec` argument invokes your custom function as a function literal that passes `point` into it.
// function threelobe2d(point) =
// let(
// ang=atan2(point.y, point.x),
// dist=norm([point.x,point.y])*(1.3+cos(3*ang))
// ) 3/dist;
// metaballs2d(
// spec = [
// IDENT, function (point) threelobe2d(point),
// IDENT, mb_circle(r=3)
// ],
// bounding_box = [[-14,-12],[8,12]],
// pixel_size=0.5);
// Example(2D): Analogous to the 3D Example 21 above, here is a 2D function nearly identical to the previous example, introducing additional dimensional parameters into the function to control its size and number of lobes. If you expiriment with this using different argument values, you should increase the bounding box along with pixel size.
// function multilobe2d(point, size, lobes) =
// let(
// ang=atan2(point.y, point.x),
// dist = norm([point.x,point.y])
// * (1.3+cos(lobes*ang))
// ) size/dist;
// metaballs2d(
// spec = [
// left(7),
// function (point) multilobe2d(point,3,4),
// right(7)*zrot(60),
// function (point) multilobe2d(point,3,3)
// ],
// bounding_box = [[-16,-13],[18,13]],
// pixel_size=0.4);
// Example(2D,Med,NoAxes: Demonstration of `debug=true` with a variety of metaball shapes. The metaballs themselves are shown as outlines, with the underlying primitive shape shown in blue (for positive metaballs) or orange (for negative metaballs).
assert(all_defined([spec,bounding_box]),"\nThe parameters spec and bounding_box must both be defined.")
assert(is_num(bounding_box)||len(bounding_box[0])==2,"\nBounding box must be 2D.")
assert(num_defined([pixel_size,pixel_count])<=1,"\nOnly one of pixel_size or pixel_count can be defined.")
assert(is_undef(pixel_size)||(is_finite(pixel_size)&&pixel_size>0)||(is_vector(pixel_size)&&all_positive(pixel_size)),"\npixel_size must be a positive number, a 2-vector of positive values, or not given.")
assert(is_finite(isovalue)||(is_list(isovalue)&&len(isovalue)==2&&is_num(isovalue[0])&&is_num(isovalue[1])),"\nIsovalue must be a number or a range; a number is the same as [number,INF].")
assert(len(spec)%2==0,"\nThe spec parameter must be an even-length list of alternating transforms and functions")
// To provide a function, you supply a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals)
// bounding_box = The volume in which to perform computations, expressed as a scalar size of a cube centered on the origin, or a pair of 3D points `[[xmin,ymin,zmin], [xmax,ymax,zmax]]` specifying the minimum and maximum box corner coordinates. Unless you set `exact_bounds=true`, the bounding box size may be enlarged to fit whole voxels. When `f` is an array of values, `bounding_box` cannot be supplied if `voxel_size` is supplied because the bounding box is already implied by the array size combined with `voxel_size`, in which case this implied bounding box is centered around the origin.
// voxel_size = Size of the voxels used to sample the bounding box volume, can be a scalar or 3-vector, or omitted if `voxel_count` is set. You may get non-cubical voxels of a slightly different size than requested if `exact_bounds=true`.
// voxel_count = Approximate number of voxels in the bounding box. If `exact_bounds=true` then the voxels may not be cubes. Use with `show_stats=true` to see the corresponding voxel size. Default: 10000 (if `voxel_size` not set)
// closed = When true, close the surface if it intersects the bounding box by adding a closing face. When false, do not add a closing face and instead produce a non-manfold VNF that has holes. Default: true
// reverse = When true, reverses the orientation of the VNF faces. Default: false
// exact_bounds = When true, shrinks voxels as needed to fit whole voxels inside the requested bounding box. When false, enlarges `bounding_box` as needed to fit whole voxels of `voxel_size`, and centers the new bounding box over the requested box. Default: false
// show_stats = If true, display statistics in the console window about the isosurface: number of voxels that the surface passes through, number of triangles, bounding box of the voxels, and voxel-rounded bounding box of the surface, which may help you reduce your bounding box to improve speed. Enabling this parameter has a slight speed penalty. Default: false
// show_box = (Module only) display the requested bounding box as transparent. This box may appear slightly inside the bounds of the figure if the actual bounding box had to be expanded to accommodate whole voxels. Default: false
// cp = (Module only) Center point 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 = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// orient = (Module only) Vector to rotate top toward, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
// Example(3D,VPD=85,VPT=[0,0,2],VPR=[55,0,30]): These first three examples demonstrate the effect of isovalue range for the simplest of all surfaces: a sphere where $r=\sqrt{x^2+y^2+z^2}$, or `r = norm([x,y,z])` in OpenSCAD. Then, the isosurface corresponding to an isovalue of 10 is every point where the expression `norm([x,y,z])` equals a radius of 10. We use the isovalue range `[-INF,10]` here to make the sphere, with a bounding box that cuts off half the sphere. The isovalue range could also be `[0,10]` because the minimum value of the expression is zero.
// Example(3D,VPD=85,VPT=[0,0,2],VPR=[55,0,30]): Here we set the isovalue range to `[10,INF]`. Because the sphere expression `norm(xyz)` has larger values growing to infinity with distance from the origin, the resulting object appears as the bounding box with a radius-10 spherical hole.
// Example(3D,ThrownTogether,NoAxes): Unlike a sphere, a gyroid is unbounded; it's an isosurface defined by all the zero values of a 3D periodic function. To illustrate what the surface looks like, `closed=false` has been set to expose both sides of the surface. The surface is periodic and tileable along all three axis directions. This is a non-manifold surface as displayed, not useful for 3D modeling. This example also demonstrates using an additional parameter in the field function beyond just the `[x,y,z]` input; in this case to control the wavelength of the gyroid.
// Example(3D,NoAxes): If we remove the `closed` 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.
// 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 `closed=false` the edges are not closed where the surface is clipped by the bounding box.
// Example(3D,ThrownTogether,NoAxes): To make the gyroid a valid manifold 3D object, we remove the `closed` parameter (same as setting `closed=true`), which closes the edges where the surface is clipped by the bounding box.
// Example(3D,NoAxes): An approximation of the triply-periodic minimal surface known as [Schwartz P](https://en.wikipedia.org/wiki/Schwarz_minimal_surface).
// Example(3D,Med,NoAxes,VPD=165,VPR=[72,0,290],VPT=[0,0,0]): An object that could be a sort of support pillar. Here we set `show_box=true` to reveal that the bounding box is slightly bigger than it needs to be. The argument `show_stats=true` also outputs the voxel bounding box size as a suggestion of what it should be.
// Example(3D,Med,NoAxes,VPD=47,VPT=[0,0,2]): You can specify non-cubical voxels for efficiency. This example shows the result of two identical surface functions. The figure on the left uses a `voxel_size=1`, which washes out the detail in the z direction. The figure on the right shows the same shape with `voxel_size=[0.5,1,0.2]` to give a bit more resolution in the x direction and much more resolution in the z direction. This example runs about six times faster than if we used a cubical voxel of size 0.2 to capture the detail in only one axis at the expense of unnecessary detail in other axes.
// Example(3D,NoAxes,VPD=50,VPT=[2,0,1]): Nonlinear functions with steep gradients between voxel corners at the isosurface value can show interpolation ridges because the surface position is approximated by a linear interpolation of a highly nonlinear function. The appearance of the artifacts depends on the combination of function, voxel size, and isovalue, and can look different in different circumstances. If your isovalue is positive, then you may be able to smooth out the artifacts by using the log of your function and the log of your isovalue range to get the same isosurface without artifacts. On the left, an isosurface around a steep nonlinear function (clipped on the left by the bounding box) exhibits severe interpolation artifacts. On the right, the log of the isosurface around the log of the function smooths it out nicely.
// Example(3D): Using an array for the `f` argument instead of a function literal. Each row of the array represents an X index for a YZ plane with the array Z indices changing fastest in each plane. The final object may need rotation to get the orientation you want. You don't pass the `bounding_box` argument here; it is implied by the array size and voxel size, and centered on the origin.
assert(num_defined([voxel_size,voxel_count])<=1,"\nOnly one of voxel_size or voxel_count can be defined.")
assert(is_undef(voxel_size)||(is_finite(voxel_size)&&voxel_size>0)||(is_vector(voxel_size)&&all_positive(voxel_size)),"\nvoxel_size must be a positive number, a 3-vector of positive values, or undef.")
assert(is_list(isovalue)&&len(isovalue)==2&&is_num(isovalue[0])&&is_num(isovalue[1]),"\nIsovalue must be a range; use [minvalue,INF] or [-INF,maxvalue] for an unbounded range.")
// Computes a [region](regions.scad) that contains one or more 2D contour [paths](paths.scad)
// within a bounding box at a single isovalue.
// .
// The contour of a function $f(x,y)$ is the set of points where $f(x,y,z)=c$ for some
// constant isovalue $c$. Considered in the context of an elevation map, the function returns an
// elevation associated with any $(x,y)$ point, and the isovalue $c$ is a specific elevation at
// which to compute the contour paths.
// To provide a function, you supply a [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals)
// taking two parameters as input to define the grid coordinate location (e.g. `x,y`) and
// returning a single numerical value.
// You can also define an contour using a 2D array of values (i.e. a height map) instead of a
// function, in which case the contour is the set of points equal to the isovalue as interpolated
// from the array. The array indices are in the order `[x][y]` with `y` changing fastest.
// .
// The contour is evaluated over a bounding box defined by its minimum and maximum corners,
// `[[xmin,ymin],[xmax,ymax]]`. This bounding box is divided into pixels of the specified
// `pixel_size`. Smaller pixels produce a finer, smoother result at the expense of execution time.
// If the pixel size doesn't exactly divide your specified bounding box, then the bounding box is
// enlarged to contain whole pixels, and centered on your requested box. If the bounding box clips
// the contour and `closed=true` (the default), additional edges are added along the edges of the
// bounds. Setting `closed=false` causes a clipped path to end at the bounding box.
// .
// The `pixel_size` and `bounding_box` parameters affect the run time, although not as severely
// as with {{isosurface()}}. A bounding box that is larger than your contour wastes time computing
// function values that are not needed. If the contour fits completely within the bounding box, you can
// call {{pointlist_bounds()}} on all paths inside the region returned from the `contour()` function to get an
// idea of a the optimal bounding box to use. You may be able to decrease run time, or keep the
// same run time but increase the resolution. You can also set the parameter `show_stats=true` to
// get the bounds of the pixels containing the surface.
// Arguments:
// f = The contour function or array.
// isovalue = a scalar giving the isovalue parameter.
// bounding_box = The area in which to perform computations, expressed as a scalar size of a square centered on the origin, or a pair of 2D points `[[xmin,ymin], [xmax,ymax]]` specifying the minimum and maximum box corner coordinates. Unless you set `exact_bounds=true`, the bounding box size may be enlarged to fit whole pixels. When `f` is an array of values, `bounding_box` cannot be supplied if `pixel_size` is supplied because the bounding box is already implied by the array size combined with `pixel_size`, in which case this implied bounding box is centered around the origin.
// pixel_size = Size of the pixels used to sample the bounding box volume, can be a scalar or 2-vector, or omitted if `pixel_count` is set. You may get rectangular pixels of a slightly different size than requested if `exact_bounds=true`.
// ---
// pixel_count = Approximate number of pixels in the bounding box. If `exact_bounds=true` then the pixels may not be square. Use with `show_stats=true` to see the corresponding pixel size. Default: 1024 (if `pixel_size` not set)
// px_centers = When true, uses the center value of each pixel as an additional data point to refine the contour path through the pixel. The center value is the function value if `f` is a function, or the average of the four pixel corners if `f` is an array. Default: true
// closed = When true, close the contour path if it intersects the bounding box by adding closing edges. When false, do not add closing edges. Default: true
// exact_bounds = When true, shrinks pixels as needed to fit whole pixels inside the requested bounding box. When false, enlarges `bounding_box` as needed to fit whole pixels of `pixel_size`, and centers the new bounding box over the requested box. Default: false
// show_stats = If true, display statistics in the console window about the contour: number of pixels that the surface passes through, number of points in all contours, bounding box of the pixels, and pixel-rounded bounding box of the contours, which may help you reduce your bounding box to improve speed. Default: false
// anchor = (Module only) Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `"origin"`
// spin = (Module only) Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// Example(2D,NoAxes): A small height map consisting of 8×8 data values to create a 7×7 pixel area, showing a contour at one isovalue. When passing an array as a function, rotating the output 90° clockwise using `zrot(-90)` causes the features of the contour to correspond visually to features in the array. Setting `px_centers=false` results in only the corner values of each pixel to be considered when drawing contour lines, resulting in coarse outlines.
// field =[
// [0,2,2,1,0,0,0,0],
// [2,4,1,0,0,0,0,0],
// [2,2,2,1,0,0,0,0],
// [0,0,1,2,2,2,1,1],
// [0,0,2,1,0,3,1,0],
// [0,2,0,2,0,3,4,0],
// [0,0,0,1,2,3,2,0],
// [0,0,0,0,0,1,0,0]
// ];
// isoval=0.7;
// pixsize = 5;
// color("lightgreen") zrot(-90)
// contour(field, isoval, pixel_size=pixsize,
// px_centers=false);
// color("blue") down(1)
// square((len(field)-1)*pixsize, true);
// Example(2D,NoAxes): The same height map with the same isovalue, this time setting `px_centers=true` to cause the pixel center values (average of the four corners) to be considered when drawing contours. This can result in somewhat finer resolution at the expense of some additional crookedness in the contours, which is more evident when the input data values are quantized (in this case quantized to integer values).