Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Richard Milewski
2025-04-04 11:28:24 -07:00
12 changed files with 738 additions and 374 deletions

View File

@@ -1053,6 +1053,8 @@ module attach(parent, child, overlap, align, spin=0, norot, inset=0, shiftout=0,
* rot(from=parent_abstract_anchor[2],to=UP)
* rot(v=anchor,-spin),
align);
spinaxis = two_d? UP : anchor_dir;
olap = - overlap * reference - inset*inset_dir + shiftout * (inset_dir + factor*reference);
if (norot || (approx(anchor_dir,reference) && anchor_spin==0))
@@ -3532,7 +3534,8 @@ function attach_geom(
)
)
) :
["point", cp, offset, anchors];
two_d? ["point2d", cp, offset, anchors]
: ["point", cp, offset, anchors];
@@ -3554,7 +3557,7 @@ function attach_geom(
function _attach_geom_2d(geom) =
let( type = geom[0] )
type == "trapezoid" || type == "ellipse" ||
type == "rgn_isect" || type == "rgn_extent";
type == "rgn_isect" || type == "rgn_extent" || type=="point2d";
/// Internal Function: _attach_geom_size()
@@ -3567,6 +3570,7 @@ function _attach_geom_2d(geom) =
function _attach_geom_size(geom) =
let( type = geom[0] )
type == "point"? [0,0,0] :
type == "point2d"? [0,0] :
type == "prismoid"? ( //size, size2, shift, axis
let(
size=geom[1], size2=geom[2], shift=point2d(geom[3]),
@@ -3780,7 +3784,7 @@ function _attach_transform(anchor, spin, orient, geom, p) =
* affine3d_translate(point3d(-pos))
)
is_undef(p)? m
: is_vnf(p) && p==EMPTY_VNF? p
: is_vnf(p) && p==[[],[]] ? p
: apply(m, p);
@@ -3992,6 +3996,12 @@ function _find_anchor(anchor, geom)=
pos = point3d(cp) + point3d(offset),
vec = unit(anchor,UP)
) [anchor, pos, vec, oang]
) : type == "point2d"? (
let(
anchor = unit(_force_anchor_2d(anchor), [0,0]),
pos = point2d(cp) + point2d(offset),
vec = unit(anchor,BACK)
) [anchor, pos, vec, oang]
) : type == "spheroid"? ( //r
let(
rr = geom[1],
@@ -4984,7 +4994,7 @@ function _local_struct_val(struct, key)=
function _force_anchor_2d(anchor) =
is_undef(anchor) || len(anchor)==2 ? anchor :
is_undef(anchor) || len(anchor)==2 || is_string(anchor) ? anchor :
assert(anchor.y==0 || anchor.z==0, "Anchor for a 2D shape cannot be fully 3D. It must have either Y or Z component equal to zero.")
anchor.y==0 ? [anchor.x,anchor.z] : point2d(anchor);
@@ -5083,7 +5093,7 @@ module restore(desc)
// Arguments:
// desc = Description to use to get the point
// p = Point or point list to transform. Default: CENTER (if anchor not given)
// --
// ---
// anchor = Anchor point (only one) that you want to extract. Default: CENTER
// Example(3D): In this example we translate away from the parent object and then compute points on that object. Note that with OpenSCAD 2021.01 you must use union() or alternatively place the pt1 and pt2 assignments in a let() statement. This is not necessary in development versions.
// cuboid(10) let(desc=parent())
@@ -5130,7 +5140,7 @@ function desc_point(desc, p, anchor) =
// Arguments:
// desc = Description to use. Default: use the global world coordinate system
// dir = Direction or list of directions to use. Default: UP (if anchor is not given)
// --
// ---
// anchor = Anchor (only one) to get the direction from.
// Example(3D): Here we don't give a description so the reference is to the global world coordinate system, and we don't give a direction, so the default of UP applies. This lets the cylinder be placed so it is horizontal in world coordinates.
// prismoid(20,10,h=15)
@@ -5190,9 +5200,13 @@ function desc_attach(desc, anchor=UP, p, reverse=false) =
// desc2 = Second description
// anchor2 = Anchor for second description
// Example(3D): Computes the distance between a point on each cube.
// cuboid(10) let(desc=parent())
// right(15) cuboid(10)
// echo(desc_dist(parent(),TOP+RIGHT+BACK, desc, TOP+LEFT+FWD));
// cuboid(10) let(desc=parent()) {
// color("red")attach(TOP+LEFT+FWD) sphere(r=0.75,$fn=12);
// right(15) cuboid(10) {
// color("red") attach(TOP+RIGHT+BACK) sphere(r=0.75,$fn=12);
// echo(desc_dist(parent(),TOP+RIGHT+BACK, desc, TOP+LEFT+FWD)); // Prints 26.9258
// }
// }
function desc_dist(desc1,anchor1=CENTER, desc2, anchor2=CENTER)=
assert(is_description(desc1),"Invalid description: desc1")
@@ -5261,9 +5275,9 @@ function transform_desc(T,desc) =
// .
// The descriptions are made available through function literals provided in the `$` variables. The
// available functions are
// * $next([di], [desc]): Returns the description of the next object, or if i is given, the object i steps forward. The indexing wraps around.
// * $prev([di], [desc]): Returns the description of the previoud object, or if i is given, the object i steps before. The indexing wraps around.
// * $desc(i, [desc]): Returns a description of the object with index `i`. Indexing does not wrap around.
// * $next([di], [desc]): Returns the description of the next object, or if `di` is given, the object `di` steps forward. The indexing wraps around.
// * $prev([di], [desc]): Returns the description of the previous object, or if `di` is given, the object `di` steps before. The indexing wraps around.
// * $desc(i, [desc]): Returns a description of the object with index `i`. Indexing does **not** wrap around.
// All of these functions have an optional `desc` parameter, which is the description that will be transformed to produce the next, previous, or indexed
// description. By default `desc` is set to {{parent()}}, but you may wish to use a different description if you have objects that vary.
// .

View File

@@ -299,7 +299,8 @@ module stroke(
// We want to allow "paths" with length 1, so we can't use the normal path/region checks
paths = is_matrix(path) ? [path] : path;
assert(is_list(paths),"The path argument must be a list of 2D or 3D points, or a region.");
attachable(){
attachable(two_d=true)
{
for (path = paths) {
pathvalid = is_path(path,[2,3]) || same_shape(path,[[0,0]]) || same_shape(path,[[0,0,0]]);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 938 KiB

View File

@@ -1010,6 +1010,40 @@ _MTriSegmentTable = [ // marching triangle segment table
[[], []] //31 - 11111
];
_MTriSegmentTable_reverse = [
[[],[]],
[[3,4,0],[]],
[[0,5,1],[]],
[[3,4,5,1],[]],
[[2,7,3],[]],
[[2,7,4,0],[]],
[[0,5,1],[2,7,3]],
[[2,7,4,5,1],[]],
[[1,6,2],[]],
[[3,4,0],[1,6,2]],
[[0,5,6,2],[]],
[[3,4,5,6,2],[]],
[[1,6,7,3],[]],
[[1,6,7,4,0],[]],
[[0,5,6,7,3],[]],
[[7,4,5,6,7],[]],
[[4,7,6,5,4],[]],
[[3,7,6,5,0],[]],
[[0,4,7,6,1],[]],
[[3,7,6,1],[]],
[[2,6,5,4,3],[]],
[[2,6,5,0],[]],
[[2,6,1],[0,4,3]],
[[2,6,1],[]],
[[1,5,4,7,2],[]],
[[1,5,0],[3,7,2]],
[[0,4,7,2],[]],
[[3,7,2],[]],
[[1,5,4,3],[]],
[[1,5,0],[]],
[[0,4,3],[]],
[[],[]]
];
/*
Low-res "marching squares" case has the same labeling but without the center vertex
and extra edges. In the two ambiguous cases with two opposite corners above and the
@@ -1043,6 +1077,25 @@ _MSquareSegmentTable = [ // marching square segment table (lower res)
[[], []] //15 - 1111
];
_MSquareSegmentTable_reverse = [
[[],[]],
[[3,0],[]],
[[0,1],[]],
[[3,1],[]],
[[2,3],[]],
[[2,0],[]],
[[2,1],[0,3]],
[[2,1],[]],
[[1,2],[]],
[[1,0],[3,2]],
[[0,2],[]],
[[3,2],[]],
[[1,3],[]],
[[1,0],[]],
[[0,3],[]],
[[],[]]
];
/// _mctrindex() - private function
/// Return the index ID of a pixel depending on the field strength at each vertex exceeding isoval.
function _mctrindex(f, isoval) =
@@ -1065,7 +1118,7 @@ function _bbox_sides(pc, pixsize, bbox) = let(
];
function _contour_pixels(pixsize, bbox, fieldarray, fieldfunc, pixcenters, isovalue, closed=true) = let(
function _contour_pixels(pixsize, bbox, fieldarray, fieldfunc, pixcenters, isovalmin, isovalmax, closed=true) = let(
// get field intensities
hp = 0.5*pixsize,
field = is_def(fieldarray)
@@ -1080,16 +1133,19 @@ function _contour_pixels(pixsize, bbox, fieldarray, fieldfunc, pixcenters, isova
nx = len(field)-2,
ny = len(field[0])-2,
v0 = bbox[0]
) let(isocorrect = sign(isovalue)*max(abs(isovalue)*1.000001, isovalue+0.0000001)) [
) let(
isocorrectmin = (isovalmin>=0?1:-1)*max(abs(isovalmin)*1.000001, isovalmin+0.0000001),
isocorrectmax = (isovalmax>=0?1:-1)*max(abs(isovalmax)*1.000001, isovalmax+0.0000001)
) [
for(i=[0:nx]) let(x=v0.x+pixsize.x*i)
for(j=[0:ny]) let(y=v0.y+pixsize.y*j)
let(i1=i+1, j1=j+1,
pf = let(
// clamp corner values to ±1e9, make sure no corner=isovalue
f0=let(c=min(1e9,max(-1e9,field[i][j]))) abs(c-isovalue)<EPSILON ? isocorrect : c,
f1=let(c=min(1e9,max(-1e9,field[i][j1]))) abs(c-isovalue)<EPSILON ? isocorrect : c,
f2=let(c=min(1e9,max(-1e9,field[i1][j]))) abs(c-isovalue)<EPSILON ? isocorrect : c,
f3=let(c=min(1e9,max(-1e9,field[i1][j1]))) abs(c-isovalue)<EPSILON ? isocorrect : c
// clamp corner values to ±1e9, make sure no corner=isovalmin or isovalmax
f0=let(c=min(1e9,max(-1e9,field[i][j]))) abs(c-isovalmin)<EPSILON ? isocorrectmin : abs(c-isovalmax)<EPSILON ? isocorrectmax : c,
f1=let(c=min(1e9,max(-1e9,field[i][j1]))) abs(c-isovalmin)<EPSILON ? isocorrectmin : abs(c-isovalmax)<EPSILON ? isocorrectmax : c,
f2=let(c=min(1e9,max(-1e9,field[i1][j]))) abs(c-isovalmin)<EPSILON ? isocorrectmin : abs(c-isovalmax)<EPSILON ? isocorrectmax : c,
f3=let(c=min(1e9,max(-1e9,field[i1][j1]))) abs(c-isovalmin)<EPSILON ? isocorrectmin : abs(c-isovalmax)<EPSILON ? isocorrectmax : c
) [ // pixel corner field values
f0, f1, f2, f3,
// get center value of pixel
@@ -1100,33 +1156,39 @@ function _contour_pixels(pixsize, bbox, fieldarray, fieldfunc, pixcenters, isova
? min(1e9,max(-1e9,fieldfunc(x+hp.x, y+hp.y)))
: 0.25*(f0 + f1 + f2 + f3)
],
minpf = min(pf),
maxpf = max(pf),
pixcoord = [x,y],
pixfound_isoval = (min(pf) <= isovalue && isovalue <= max(pf)),
psides = closed ? _bbox_sides(pixcoord, pixsize, bbox) : [],
pixfound_isomin = (minpf <= isovalmin && isovalmin <= maxpf),
pixfound_isomax = (minpf <= isovalmax && isovalmax <= maxpf),
pixfound_outer = len(psides)==0 ? false
: let(
ps = flatten([for(i=psides) _MTEdgeVertexIndices[i]]),
sumcond = len([for(p=ps) if(isovalue<=pf[p]) 1])
) sumcond == len(ps), // true if full edge is <= isovalue
pixindex = pixfound_isoval ? _mctrindex(pf, isovalue) : 0
) if(pixfound_isoval || pixfound_outer) [
pixcoord,
pixindex,
pf, psides
sumcond = len([for(p=ps) if(isovalmin<=pf[p] && pf[p]<=isovalmax) 1])
) sumcond == len(ps), // true if full edge is between isovalmin and isovalmax
pixindex_isomin = pixfound_isomin ? _mctrindex(pf, isovalmin) : 0,
pixindex_isomax = pixfound_isomax ? _mctrindex(pf, isovalmax) : 0
) if(pixfound_isomin || pixfound_isomax || pixfound_outer) [
pixcoord, // pixel lower coordinate
pixindex_isomin, // pixel ID for isomin
pixindex_isomax, // pixel ID for isomax
pf, // clamped pixel corner values
psides // list of bounding box sides, if any
]
];
function _contour_vertices(pxlist, pxsize, isoval, segtable=_MTriSegmentTable) = [
function _contour_vertices(pxlist, pxsize, isovalmin, isovalmax, segtablemin, segtablemax) = [
for(px = pxlist) let(
v = px[0],
idx = px[1],
f = px[2],
bbsides = px[3],
vpix = [ v, v+[0,pxsize.y], v+[pxsize.x,0], v+[pxsize.x,pxsize.y], v+0.5*[pxsize.x,pxsize.y] ],
spath = segtable[idx]
idxmin = px[1],
idxmax = px[2],
f = px[3],
bbsides = px[4],
vpix = [ v, v+[0,pxsize.y], v+[pxsize.x,0], v+[pxsize.x,pxsize.y], v+0.5*[pxsize.x,pxsize.y] ]
) each [
for(sp=spath)
for(sp=segtablemin[idxmin]) // min contour
if(len(sp)>0) [
for(p=sp)
let(
@@ -1134,7 +1196,18 @@ function _contour_vertices(pxlist, pxsize, isoval, segtable=_MTriSegmentTable) =
vi0 = edge[0],
vi1 = edge[1],
denom = f[vi1] - f[vi0],
u = abs(denom)<0.00001 ? 0.5 : (isoval-f[vi0]) / denom
u = abs(denom)<0.00001 ? 0.5 : (isovalmin-f[vi0]) / denom
) vpix[vi0] + u*(vpix[vi1]-vpix[vi0])
],
for(sp=segtablemax[idxmax]) // max contour
if(len(sp)>0) [
for(p=sp)
let(
edge = _MTEdgeVertexIndices[p],
vi0 = edge[0],
vi1 = edge[1],
denom = f[vi1] - f[vi0],
u = abs(denom)<0.00001 ? 0.5 : (isovalmax-f[vi0]) / denom
) vpix[vi0] + u*(vpix[vi1]-vpix[vi0])
],
if(len(bbsides)>0) for(b = bbsides)
@@ -1142,55 +1215,29 @@ function _contour_vertices(pxlist, pxsize, isoval, segtable=_MTriSegmentTable) =
edge = _MTEdgeVertexIndices[b],
vi0 = edge[0],
vi1 = edge[1],
denom = f[vi1] - f[vi0],
u = abs(denom)<0.00001 ? 0.5 : (isoval-f[vi0]) / denom,
midpt = vpix[vi0] + u*(vpix[vi1]-vpix[vi0])
) if(f[vi0]>=isoval && f[vi1]>=isoval) [vpix[vi0], vpix[vi1]]
else if(f[vi0] >= isoval) [vpix[vi0], midpt]
else if(f[vi1]>=isoval) [midpt, vpix[vi1]]
rev = f[vi0]<f[vi1],
f0 = f[vi0],
f1 = f[vi1],
p0 = vpix[vi0],
p1 = vpix[vi1],
denom = f1 - f0,
umin = abs(denom)<0.00001 ? 0.5 : max(-1e9, min(1e9, isovalmin-f0)) / denom,
umax = abs(denom)<0.00001 ? 0.5 : max(-1e9, min(1e9, isovalmax-f0)) / denom,
midptmin = p0 + umin*(p1-p0),
midptmax = p0 + umax*(p1-p0)
)
if(f0<=isovalmin && isovalmin<=f1 && f1<=isovalmax) [midptmin, p1]
else if(f0>=isovalmax && isovalmax>=f1 && f1>=isovalmin) [midptmax, p1]
else if(f1>=isovalmax && isovalmax>=f0 && f0>=isovalmin) [p0, midptmax]
else if(f1<=isovalmin && isovalmin<=f0 && f0<=isovalmax) [p0, midptmin]
else if(f0<isovalmin && f1>isovalmax) [midptmin, midptmax]
else if(f0>isovalmax && f1<isovalmin) [midptmax, midptmin]
else if((f0<f1 && isovalmin<=f0 && isovalmax>=f1) || (f1<f0 && isovalmin<=f1 && isovalmax>=f0))
[p0, p1]
]
];
function _assemble_partial_paths(paths,closed=false,eps=EPSILON) =
let(
pathlist = _assemble_partial_paths_recur(paths) /*,
// this eliminates crossing paths - commented out now that it's no longer possible for the input segments to cross
splitpaths =
[for(path=pathlist) each
let(
searchlist = vector_search(path,eps,path),
duplist = [for(i=idx(searchlist)) if (len(searchlist[i])>1) i]
)
duplist==[] ? [path]
:
let(
fragments = [for(i=idx(duplist)) select(path, duplist[i], select(duplist,i+1))]
)
len(fragments)==1 ? fragments
: _assemble_path_fragments(fragments)
]*/
)
//[for(path=splitpaths) list_unwrap(path)];
closed ? [for(path=pathlist) list_unwrap(path)] : pathlist;
function _assemble_partial_paths_recur(edges, paths=[],i=0) =
i==len(edges) ? paths :
norm(edges[i][0]-last(edges[i]))<EPSILON ? _assemble_partial_paths_recur(edges,paths,i+1) :
let( // Find paths that connects on left side and right side of the edges (if one exists)
left = [for(j=idx(paths)) if (approx(last(paths[j]),edges[i][0])) j],
right = [for(j=idx(paths)) if (approx(last(edges[i]),paths[j][0])) j]
)
let(
keep_path = list_remove(paths,[if (len(left)>0) left[0],if (len(right)>0) right[0]]),
update_path = left==[] && right==[] ? edges[i]
: left==[] ? concat(list_head(edges[i]),paths[right[0]])
: right==[] ? concat(paths[left[0]],slice(edges[i],1,-1))
: left[0] != right[0] ? concat(paths[left[0]],slice(edges[i],1,-2), paths[right[0]])
: concat(paths[left[0]], slice(edges[i],1,-1)) // last arg -2 removes duplicate endpoints but this is handled in passthrough function
)
_assemble_partial_paths_recur(edges, concat(keep_path, [update_path]), i+1);
/// ---------- 3D metaball stuff starts here ----------
@@ -1655,7 +1702,7 @@ function debug_tetra(r) = let(size=r/norm([1,1,1])) [
// isolation, but if another metaball object is nearby, the two objects interact, growing larger
// and melding together. The closer the objects are, the more they blend and meld.
// .
// The `metaballs()` module and function produces scenes of 3D metaballs. The `metaballs2d()` module and
// The `metaballs()` module and function produce scenes of 3D metaballs. The `metaballs2d()` module and
// function produces scenes of 2D metaballs. The metaball specification method, tranformations, bounding box,
// and other parameters are used the say way in 3D and 2D, but in 2D, pixels replace voxels. This
// introductory section describes features common to both 3D and 2D cases.
@@ -1678,7 +1725,7 @@ function debug_tetra(r) = let(size=r/norm([1,1,1])) [
// matrices and metaball functions. It can also be a list of alternating transforms and *other specs*,
// as `[trans0, spec0, trans1, spec1, ...]`, in which `spec0`, `spec1`, etc. can be one of:
// * A built-in metaball function name as described below, such as `mb_sphere(r=10)`.
// * A function literal accepting a 3-vector representing a point in space relative to the metaball's center.
// * A function literal accepting a 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.
// * Another spec array, for nesting metaball specs together.
// .
@@ -1694,12 +1741,12 @@ function debug_tetra(r) = let(size=r/norm([1,1,1])) [
// .
// **Parameters `bounding_box` and grid units:** The metaballs are evaluated over a bounding box. The `bounding_box` parameter can be specified by
// its minimum and maximum corners: `[[xmin,ymin,zmin],[xmax,ymax,zmax]]` in 3D, or
// `[[xmin,ymin],[xmax,ymax]]` in 2D. The bounding box can also be specified as a scalar of a cube (in 3D)
// `[[xmin,ymin],[xmax,ymax]]` in 2D. The bounding box can also be specified as a scalar size of a cube (in 3D)
// or square (in 2D) centered on the origin. The contributions from **all** metaballs, even those outside
// the box, are evaluated over the bounding box.
// .
// This bounding box is divided into grid units, specified as `voxel_size` in 3D or `pixel_size` in 2D,
// either of which can also be a scalar or a vector size.
// which can be a scalar or a vector size.
// Alternately, you can set the grid count (`voxel_count` or `pixel_count`) to fit approximately the
// specified number of grid units into the bounding box.
// .
@@ -1711,7 +1758,8 @@ function debug_tetra(r) = let(size=r/norm([1,1,1])) [
// resulting in non-square grid units. Either way, if the bounding box clips a metaball and `closed=true`
// (the default), the object is closed at the intersection. Setting `closed=false` causes the object to end
// at the bounding box. In 3D, this results in a non-manifold shape with holes, exposing the inside of the
// object. In 2D, this results in an open-ended contour path with abiguity in how the path might be closed.
// object. In 2D, this results in an open-ended contour path with higher values on the right with respect to
// the path direction.
// .
// For metaballs with flat surfaces or sides, avoid letting any side of the bounding box coincide with one
// of these flat surfaces or sides, otherwise unpredictable triangulation around the edge may result.
@@ -1737,13 +1785,14 @@ function debug_tetra(r) = let(size=r/norm([1,1,1])) [
// .
// ***Metaballs run time***
// .
// The size of the grid units and size of the bounding box affects the run time, which can be long.
// The size of the grid units (voxels or pixels) and size of the bounding box affects the run time, which can
// be long, especially in 3D.
// Smaller grid units produce a finer, smoother result at the expense of execution time. Larger grid units
// shorten execution time.
// The affect on run time is most evident for 3D metaballs, less so for 2D metaballs.
// .
// For example, in 3D, a voxel size of 1 with a bounding box volume of 200×200×200 may be slow because it
// requires the calculation and storage of 8,000,000 function values, and more processing and memory to
// requires the calculation and storage of eight million function values, and more processing and memory to
// generate the triangulated mesh. On the other hand, a voxel size of 5 over a 100×100×100 bounding box
// requires only 8,000 function values and a modest computation time. A good rule is to keep the number
// of voxels below 10,000 for preview, and adjust the voxel size smaller for final rendering. If you don't
@@ -1771,7 +1820,7 @@ function debug_tetra(r) = let(size=r/norm([1,1,1])) [
// 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 3D spherical metaball and 2D circular metaball
// therefore has a simple basic definition as $f(v) = 1/\text{norm}(v)$. If we choose an isovalue $c$,
// therefore have 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.
@@ -1836,8 +1885,7 @@ function debug_tetra(r) = let(size=r/norm([1,1,1])) [
// ball. Also, depending on the value of `influence`, a cutoff that ends in the middle of
// another ball can result in strange shapes, as shown in Example 17, with the metaball
// interacting on one side of the boundary and not interacting on the other side. If you scale
// a ball, the cutoff value is also scaled. The exact way that cutoff is defined
// geometrically varies for different ball types; see below for details.
// a ball, the cutoff value is also scaled.
// .
// The `influence` parameter adjusts the strength of the interaction that metaball objects have with
// each other. If you increase `influence` of one metaball from its default of 1, then that metaball
@@ -1908,7 +1956,7 @@ function debug_tetra(r) = let(size=r/norm([1,1,1])) [
// 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
// show_box = (Module only) Display the requested bounding box as transparent. This box may appear slightly different than specified 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"`
@@ -2436,8 +2484,14 @@ module metaballs(spec, bounding_box, voxel_size, voxel_count, isovalue=1, closed
children();
}
if(show_box)
let(bbox = _getbbox(voxel_size, bounding_box, exact_bounds, undef))
%translate(bbox[0]) cube(bbox[1]-bbox[0]);
let(
bbox0 = is_num(bounding_box)
? let(hb=0.5*bounding_box) [[-hb,-hb,-hb],[hb,hb,hb]]
: bounding_box,
autovoxsize = is_def(voxel_size) ? voxel_size : _getautovoxsize(bbox0, default(voxel_count,22^3)),
voxsize = _getvoxsize(autovoxsize, bbox0, exact_bounds),
bbox = _getbbox(voxsize, bounding_box, exact_bounds, undef)
) %translate(bbox[0]) cube(bbox[1]-bbox[0]);
}
function metaballs(spec, bounding_box, voxel_size, voxel_count, isovalue=1, closed=true, exact_bounds=false, show_stats=false, _debug=false) =
@@ -2721,7 +2775,7 @@ function mb_ring(r1,r2, cutoff=INF, influence=1, negative=false, hide_debug=fals
// ![Metaball animation](https://raw.githubusercontent.com/BelfrySCAD/BOSL2/master/images/metaball_demo2d.gif)
// .
// 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 list containing one or more
// module, a 2D metaball scene is displayed. When called as a function, a [region](regions.scad) or list of
// [paths](paths.scad) is returned.
// .
// For a full explanation of metaballs, see [introduction](#section-metaballs) above. The
@@ -2737,7 +2791,7 @@ function mb_ring(r1,r2, cutoff=INF, influence=1, negative=false, hide_debug=fals
// .
// 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
// 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`.
// .
// The `cutoff` parameter specifies the distance beyond which the metaball has no interaction
@@ -2746,8 +2800,7 @@ function mb_ring(r1,r2, cutoff=INF, influence=1, negative=false, hide_debug=fals
// zero at the cutoff. Depending on the value of `influence`, a cutoff that ends in the middle of
// another ball can result in strange shapes, as shown in Example 9, with the metaball
// interacting on one side of the boundary and not interacting on the other side. If you scale
// a ball, the cutoff value is also scaled. The exact way that cutoff is defined
// geometrically varies for different ball types; see below for details.
// a ball, the cutoff value is also scaled.
// .
// The `influence` parameter adjusts the strength of the interaction that metaball objects have with
// each other. If you increase `influence` of one metaball from its default of 1, then that metaball
@@ -2782,8 +2835,8 @@ function mb_ring(r1,r2, cutoff=INF, influence=1, negative=false, hide_debug=fals
// .
// * `mb_circle(r|d=)` &mdash; 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=])` &mdash; 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 (circular) to 1 (square), 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, w1|w=, w2|w=, [ang=], [rounding=])` &mdash; rounded trapezoid metaball with arguments similar to {{trapezoid()}}. Any three of the arguments `h` (height), `w1` (bottoms width), `w2` (top width), or `ang` (bottom corner angle) may be specified, and `w` sets both `w1` and `w2` to the same size. 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 are less flexible but has faster execution time.
// * `mb_stadium(size)` &mdash; rectangle with rounded caps on the narrow ends. The object is a convex hull of two circles. The `size` parameter is normally a `[width,height]` vector, with the larger dimension specifying the distance between the ends of the circular caps. If passed as a scalar, you get a circle.
// * `mb_trapezoid(h, w1|w=, w2|w=, [ang=], [rounding=])` &mdash; rounded trapezoid metaball with arguments similar to {{trapezoid()}}. Any three of the arguments `h` (height), `w1` (bottom width), `w2` (top width), or `ang` (bottom corner angle) may be specified, and `w` sets both `w1` and `w2` to the same size. 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 are less flexible but have faster execution time.
// * `mb_stadium(size)` &mdash; rectangle with rounded caps on the narrow ends. The object is a convex hull of two circles. Set the `size` parameter to `[width,height]` to get an object that fits inside a rectangle of that size. Giving a scalar size produces a circle.
// * `mb_connector2d(p1, p2, [r|d=])` &mdash; a stadium shape specified to connect point `p1` to point `p2` (which must be different 2D coordinates). As with `mb_stadium()`, the object is a convex hull of two circles. 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(r1|d1=, r2|d2=)` &mdash; 2D ring metaball using a subset of {{ring()}} parameters, with inner radius being the smaller of `r1` and `r2`, and outer radius being the larger of `r1` and `r2`. If `cutoff` is applied, it is measured from the circle midway between `r1` and `r2`.
// .
@@ -2796,15 +2849,18 @@ function mb_ring(r1,r2, cutoff=INF, influence=1, negative=false, hide_debug=fals
// .
// ***Closed and unclosed paths***
// .
// When `metaballs2d()` is called as a module, the parameter `closed` is unavailable and always true. When called
// as a function, the parameter `closed=true` is set by default, which causes polygon segments to be generated
// wherever a metaball is clipped by the bounding box, so that all metaballs are closed polygons. When
// `closed=true`, the list of paths returned by `metaballs2d()` is a valid [region](regions.scad) with no
// duplicated vertices in any path.
// The functional form of `metaballs2d()` supports a `closed` parameter. When `closed=true` (the default)
// and a polygon is clipped by the bounding box, the bounding box edges are included in the polygon. The
// resulting path list is a valid region with no duplicated vertices in any path. The module form of
// `metaballs2d()` always closes the polygons.
// .
// When `closed=false`, however, the list of paths returned by the `metaballs2d()` function may include a
// mixture of closed and unclosed paths, in which the closed paths can be identified as having equivalent
// start and end points (this duplication makes the path list an invalid [region](regions.scad)).
// When `closed=false`, paths that intersect the edge of the bounding box end at the bounding box. This
// means that the list of paths may include a mixture of closed and open paths. Regardless of whether
// any of the output paths are open, all closed paths have identical first and last points so that closed and
// open paths can be distinguished. You can use {{are_ends_equal()}} to determine if a path is closed. A path
// list that includes open paths is not a region, because regions are lists of closed polygons. Duplicating the
// ends of closed paths can cause problems for functions such as {{offset()}}, which would complain about
// repeated points. You can pass a closed path to {{list_unwrap()}} to remove the extra endpoint.
// 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.
@@ -2817,7 +2873,7 @@ function mb_ring(r1,r2, cutoff=INF, influence=1, negative=false, hide_debug=fals
// smoothing = Number of times to apply a 2-point moving average to the contours. This can remove small zig-zag artifacts resulting from a contour that follows the profile of a triangulated 3D surface when `use_centers` is set. Default: 2 if `use_centers=true`, 0 otherwise.
// 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 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
// show_box = (Module only) Display the requested bounding box as a transparent rectangle. This box may appear slightly different than specified 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 metaball scene rendered as outlines. Positive metaballs appear blue, negative appears orange, and any custom function with no debug polygon defined appears as a gray triangle of 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"`
@@ -2890,7 +2946,7 @@ function mb_ring(r1,r2, cutoff=INF, influence=1, negative=false, hide_debug=fals
// boundingbox = [[-7,-6], [3,6]];
// metaballs2d(spec, boundingbox, voxel_size);
// color("green") move_copies(centers) cylinder(h=1,d=1,$fn=16);
// 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.
// Example(2D,VPD=105,VPT=[0,15,0]): 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,
@@ -2899,7 +2955,7 @@ function mb_ring(r1,r2, cutoff=INF, influence=1, negative=false, hide_debug=fals
// 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.
// Example(2D,NoAxes,VPD=250,VPT=[0,8,0]): 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
@@ -2955,26 +3011,36 @@ function mb_ring(r1,r2, cutoff=INF, influence=1, negative=false, hide_debug=fals
module metaballs2d(spec, bounding_box, pixel_size, pixel_count, isovalue=1, use_centers=false, smoothing=undef, exact_bounds=false, convexity=6, cp="centroid", anchor="origin", spin=0, atype="hull", show_stats=false, show_box=false, debug=false) {
regionlist = metaballs2d(spec, bounding_box, pixel_size, pixel_count, isovalue, true, use_centers, smoothing, exact_bounds, show_stats, _debug=debug);
$metaball_pathlist = debug ? regionlist[0] : regionlist; // for possible use with children
wid = min(0.5, 0.5 * (is_num(pixel_size) ? pixel_size : 0.5*(pixel_size[0]+pixel_size[1])));
if(debug) {
// display debug polygons
for(a=regionlist[1])
if(len(a[1])>0)
color(a[0]==0 ? "gray" : a[0]>0 ? "#3399FF" : "#FF9933")
region(a[1]);
//else echo("WARNING: Empty metaball path found!");
// display metaball as outline
attachable(anchor, spin, two_d=true, region=regionlist[0], extent=atype=="hull", cp=cp) {
wid = is_def(pixel_size) ? min(0.5, 0.5 * (is_num(pixel_size) ? pixel_size : 0.5*(pixel_size[0]+pixel_size[1]))) : 0.2;
stroke(regionlist[0], width=wid, closed=true);
children();
}
} else { // debug==false, just display the metaball polygons
assert(len(regionlist)>0, "\nNo metaball polygons found! Check your isovalue.")
attachable(anchor, spin, two_d=true, region=regionlist, extent=atype=="hull", cp=cp) {
if(len(regionlist)>0)
region(regionlist, anchor=anchor, spin=spin, cp=cp, atype=atype);
children();
}
}
if(show_box)
let(bbox = _getbbox2d(pixel_size, bounding_box, exact_bounds, undef))
%translate([bbox[0][0],bbox[0][1],-0.05]) linear_extrude(0.1) square(bbox[1]-bbox[0]);
let(
bbox0 = is_num(bounding_box)
? let(hb=0.5*bounding_box) [[-hb,-hb],[hb,hb]]
: bounding_box,
autopixsize = is_def(pixel_size) ? pixel_size : _getautopixsize(bbox0, default(pixel_count,32^2)),
pixsize = _getpixsize(autopixsize, bbox0, exact_bounds),
bbox = _getbbox2d(pixsize, bbox0, exact_bounds)
) %translate([bbox[0][0],bbox[0][1],-0.05]) linear_extrude(0.1) square(bbox[1]-bbox[0]);
}
function metaballs2d(spec, bounding_box, pixel_size, pixel_count, isovalue=1, closed=true, use_centers=false, smoothing=undef, exact_bounds=false, show_stats=false, _debug=false) =
@@ -2985,7 +3051,7 @@ function metaballs2d(spec, bounding_box, pixel_size, pixel_count, isovalue=1, cl
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")
let(
isoval = is_list(isovalue) ? (is_finite(isovalue[0]) ? isovalue[0] : isovalue[1]) : isovalue,
isoval = is_list(isovalue) ? isovalue : [isovalue,INF],
funclist = _mb_unwind_list(spec, twoD=true),
nballs = len(funclist)/2,
dummycheck = [
@@ -3044,46 +3110,46 @@ function _metaballs2dfield(funclist, transmatrix, bbox, pixsize, nballs) = let(
// The isosurface of a function $f(x,y,z)$ is the set of points where $f(x,y,z)=c$ for some
// constant isovalue $c$.
// .
// Any 2D cross-section of an isosurface is a contour. The contour of a function $f(x,y)$ is the set
// of points where $f(x,y)=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.
// The contour of a function $f(x,y)$ is the set of points where $f(x,y)=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.
// Any 2D cross-section of an isosurface is a contour.
// .
// <a name="isosurface-contour-parameters"></a>
// ***Parameters common to `isosurface()` and `contour()`***
// .
// **Parameter `f` (function):** 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 a 3D coordinate `[x,y,z]` (for `isosurface()`) or a 2D coordinate `[x,y]` (for `contour()`) as
// input to define the grid coordinate location and returning a single numerical value.
// You can also define an isosurface using an array of values instead of a function, in which
// case the isosurface is the set of points equal to the isovalue as interpolated from the array.
// **Parameter `f` (function):** The [function literal](https://en.wikibooks.org/wiki/OpenSCAD_User_Manual/User-Defined_Functions_and_Modules#Function_literals)
// must take 3 parameters (x, y and z) for isosurface or two parameters (x and y) for contour, and must return a single numerical value.
// You can also define an isosurface or contour using an array of values instead of a function, in which
// case the isosurface or contour is the set of points equal to the isovalue as interpolated from the array.
// The array indices are in the order `[x][y][z]` in 3D, and `[x][y]` in 2D.
// .
// **Parameter `isovalue:`** For isosurfaces, the isovalue must be specified as a range `[c_min,c_max]`.
// For contours, the isovalue is specified as a single value; for a height field, the contour isovalue is
// its elevation.
// .
// For isosurfaces, the range can be finite or unbounded at one end, with either `c_min=-INF` or `c_max=INF`.
// The returned object is the set of points `[x,y,z]` that satisfy `c_min <= f(x,y,z) <= c_max`. If `f(x,y,z)`
// has values larger than `c_min` and values smaller than `c_max`, then the result is a shell object with two
// bounding surfaces corresponding to the isosurfaces at `c_min` and `c_max`. If `f(x,y,z) < c_max`
// everywhere (which is true when `c_max = INF`), then no isosurface exists for `c_max`, so the object
// has only one bounding surface: the one defined by `c_min`. This can result in a bounded object
// like a sphere, or it can result an an unbounded object such as all the points outside of a sphere out
// to infinity. A similar situation arises if `f(x,y,z) > c_min` everywhere (which is true when
// **Parameter `isovalue:`** The isovalue must be specified as a range `[c_min,c_max]`.
// The range can be finite or unbounded at one end, with either `c_min=-INF` or `c_max=INF`.
// For isosurface, the returned object is the set of points `[x,y,z]` that satisfy `c_min <= f(x,y,z) <= c_max`,
// or in 2D, the points `[x,y]` satisfying `c_min <= f(x,y) <= c_max`. Strictly speaking, this means the
// isosurface and contour modules don't return a single contour or isovalue by the shape **bounded** by isosurfaces
// or contours. If the function has values larger than `c_min` and values smaller than `c_max`, then the result
// is a shell object (3D) or ring object (2D) with two
// bounding surfaces/curves corresponding to the isovalues of `c_min` and `c_max`. If the function is smaller
// than `c_max` everywhere (which is true when `c_max = INF`), then no isosurface exists for `c_max`, so the object
// has only one bounding surface: the one defined by `c_min`. This can result in a bounded object&mdash;a sphere
// or circle&mdash;or an unbounded object such as all the points outside of a sphere out
// to infinity. A similar situation arises if the function is larger than `c_min` everywhere (which is true when
// `c_min = -INF`). Setting isovalue to `[-INF,c_max]` or `[c_min,INF]` always produces an object with a
// single bounding isosurface, which itself can be unbounded. To obtain a bounded object, think about
// whether the function values inside your object are smaller or larger than your isosurface value. If
// single bounding isosurface or contour, which itself can be unbounded. To obtain a bounded object, think about
// whether the function values inside your object are smaller or larger than your iso value. If
// the values inside are smaller, you produce a bounded object using `[-INF,c_max]`. If the values
// inside are larger, you get a bounded object using `[c_min,INF]`.
// inside are larger, you get a bounded object using `[c_min,INF]`. When your object is unbounded, it will
// be truncated at the bounded box, which can result in an object that looks like a simple cube.
// .
// **Parameters `bounding_box` and grid units:** The isosurface is evaluated over a bounding box. The
// **Parameters `bounding_box` and grid units:** The isosurface or contour is evaluated over a bounding box. The
// `bounding_box` parameter can be specified by its minimum and maximum corners:
// `[[xmin,ymin,zmin],[xmax,ymax,zmax]]` in 3D, or `[[xmin,ymin],[xmax,ymax]]` in 2D. The bounding box can
// also be specified as a scalar of a cube (in 3D) or square (in 2D) centered on the origin.
// .
// This bounding box is divided into grid units, specified as `voxel_size` in 3D or `pixel_size` in 2D,
// either of which can also be a scalar or a vector size.
// which can be a scalar or a vector size.
// Alternately, you can set the grid count (`voxel_count` or `pixel_count`) to fit approximately the
// specified number of grid units into the bounding box.
// .
@@ -3092,16 +3158,20 @@ function _metaballs2dfield(funclist, transmatrix, bbox, pixsize, nballs) = let(
// larger. By default, if the voxel size or pixel size doesn't exactly divide your specified bounding box,
// then the bounding box is enlarged to contain whole grid units, and centered on your requested box.
// Alternatively, you may set `exact_bounds=true` to cause the grid units to adjust in size to fit instead,
// resulting in non-square grid units. Either way, if the bounding box clips the isosurface and `closed=true`
// (the default), the object is closed at the intersection. Setting `closed=false` causes the object to end
// at the bounding box. In 3D, this results in a non-manifold shape with holes, exposing the inside of the
// object. In 2D, this results in an open-ended contour path with abiguity in how the path might be closed.
// resulting in non-square grid units.
// .
// The isosurface or contour object is clipped by the bounding box. The contour module always closes the shapes
// at the boundary to produce displayable polygons. The isosurface module and the function forms
// accept a `closed` parameter. Setting `closed=false` causes the closing segments or surfaces along the bounding
// box to be excluded from the model. In 3D, this results in a non-manifold shape with holes, exposing the inside of the
// object. In 2D, this results in an open-ended contour path with higher values on the right with respect to
// the path direction.
// .
// ***Isosurface and contour run time***
// .
// The size of the voxels or pixels, and size of the bounding box affects the run time, which can be long.
// This is usually more noticeable in 3D than 2D. In 3D, a voxel size of 1 with a bounding box volume of
// 200×200×200 may be slow because it requires the calculation and storage of 8,000,000 function values,
// 200×200×200 may be slow because it requires the calculation and storage of eight million function values,
// and more processing and memory to generate the triangulated mesh. On the other hand, a voxel size of 5
// over a 100×100×100 bounding box requires only 8,000 function values and a modest computation time. A
// good rule is to keep the number of voxels below 10,000 for preview, and adjust the voxel size smaller
@@ -3155,7 +3225,7 @@ function _metaballs2dfield(funclist, transmatrix, bbox, pixsize, nballs) = let(
// 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
// show_box = (Module only) display the requested bounding box as transparent. This box may appear slightly different than specified if the actual bounding box had to be expanded to accommodate whole voxels. Default: false
// convexity = (Module only) Maximum number of times a line could intersect a wall of the shape. Affects preview only. Default: 6
// 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"`
@@ -3320,8 +3390,15 @@ module isosurface(f, isovalue, bounding_box, voxel_size, voxel_count=undef, reve
vnf_polyhedron(vnf, convexity=convexity, cp=cp, anchor=anchor, spin=spin, orient=orient, atype=atype)
children();
if(show_box)
let(bbox = _getbbox(voxel_size, bounding_box, exact_bounds, f))
%translate(bbox[0]) cube(bbox[1]-bbox[0]);
let(
bbox0 = is_num(bounding_box)
? let(hb=0.5*bounding_box) [[-hb,-hb,-hb],[hb,hb,hb]]
: bounding_box,
autovoxsize = is_def(voxel_size) ? voxel_size : _getautovoxsize(bbox0, default(voxel_count,22^3)),
exactbounds = is_def(exact_bounds) ? exact_bounds : is_list(f),
voxsize = _mball ? voxel_size : _getvoxsize(autovoxsize, bbox0, exactbounds),
bbox = _mball ? bounding_box : _getbbox(voxsize, bbox0, exactbounds, f)
) %translate(bbox[0]) cube(bbox[1]-bbox[0]);
}
function isosurface(f, isovalue, bounding_box, voxel_size, voxel_count=undef, reverse=false, closed=true, exact_bounds=false, show_stats=false, _mball=false) =
@@ -3341,7 +3418,7 @@ function isosurface(f, isovalue, bounding_box, voxel_size, voxel_count=undef, re
isovalmin = is_list(isovalue) ? isovalue[0] : isovalue,
isovalmax = is_list(isovalue) ? isovalue[1] : INF,
dumiso1 = assert(isovalmin < isovalmax, str("\nBad isovalue range (", isovalmin, ", >= ", isovalmax, "), should be expressed as [min_value, max_value].")),
dumiso2 = assert(isovalmin != -INF || isovalmin != INF, "\nIsovalue range must be finite on one end."),
dumiso2 = assert(isovalmin != -INF || isovalmax != INF, "\nIsovalue range must be finite on one end."),
exactbounds = is_def(exact_bounds) ? exact_bounds : is_list(f),
// new voxel or bounding box centered around original, to fit whole voxels
@@ -3446,7 +3523,7 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// SynTags: Geom,Path,Region
// Topics: Isosurfaces, Path Generators (2D), Regions
// Usage: As a module
// contour(f, isovalue, bounding_box, pixel_size, [pixel_count=], [use_centers=], [smoothing=], [exact_bounds=], [show_stats=], ...) [ATTACHMENTS];
// contour(f, isovalue, bounding_box, pixel_size, [pixel_count=], [use_centers=], [smoothing=], [exact_bounds=], [show_stats=], [show_box=], ...) [ATTACHMENTS];
// Usage: As a function
// region = contour(f, isovalue, bounding_box, pixel_size, [pixel_count=], [pc_centers=], [smoothing=], [closed=], [show_stats=]);
// Description:
@@ -3473,7 +3550,8 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// .
// ***Closed and unclosed paths***
// .
// The functional form of `metaballs2d()` supports a `closed` parameter. When `closed=true` (the default)
// The module form of `contour()` always closes the polygons at the bounding box edges to produce
// valid polygons. The functional form of `contour()` supports a `closed` parameter. When `closed=true` (the default)
// and a polygon is clipped by the bounding box, the bounding box edges are included in the polygon. The
// resulting path list is a valid region with no duplicated vertices in any path.
// .
@@ -3482,11 +3560,11 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// any of the output paths are open, all closed paths have identical first and last points so that closed and
// open paths can be distinguished. You can use {{are_ends_equal()}} to determine if a path is closed. A path
// list that includes open paths is not a region, because regions are lists of closed polygons. Duplicating the
// ends of closed paths can cause problems for functions such as {{offset()}}, which would complain about
// repeated points. You can pass a closed path to {{list_unwrap()}} to remove the extra endpoint.
// ends of closed paths can cause problems for functions such as {{offset()}}, which will complain about
// repeated points or produce incorrect results. You can use {{list_unwrap()}} to remove the extra endpoint.
// Arguments:
// f = The contour function or array.
// isovalue = a scalar giving the isovalue parameter.
// isovalue = A scalar giving the isovalue for the contour, or a 2-vector giving an isovalue range (resulting in a polygon bounded by two contours). For an unbounded range, use `[-INF,max_isovalue]` or `[min_isovalue,INF]`.
// 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`.
// ---
@@ -3496,6 +3574,7 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// closed = (Function only) 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, and always true when called as a module.
// 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
// show_box = (Module only) display the requested bounding box as a transparent rectangle. This box may appear slightly different than specified if the actual bounding box had to be expanded to accommodate whole pixels. 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`
@@ -3514,7 +3593,7 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// [0,0,0,1,2,3,2,0],
// [0,0,0,0,0,1,0,0]
// ];
// isoval=0.7;
// isoval=[0.7,INF];
// pixsize = 5;
// color("lightgreen") zrot(-90)
// contour(field, isoval, pixel_size=pixsize,
@@ -3532,7 +3611,7 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// [0,0,0,1,2,3,2,0],
// [0,0,0,0,0,1,0,0]
// ];
// isoval=0.7;
// isoval=[0.7,INF];
// pixsize = 5;
// color("lightgreen") zrot(-90)
// contour(field, isoval, pixel_size=pixsize,
@@ -3549,7 +3628,7 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// translate([0,0,isoval]) color("green") zrot(-90)
// contour(function(x,y) wave2d(x,y,wavelen),
// bounding_box=[[-50,-50],[50,50]],
// isovalue=isoval, pixel_size=pixsize);
// isovalue=[isoval,INF], pixel_size=pixsize);
//
// %heightfield(size=[100,100], bottom=-45, data=[
// for (y=[-50:pixsize:50]) [
@@ -3557,17 +3636,65 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// wave2d(x,y,wavelen)
// ]
// ], style="quincunx");
// Example(2D,NoAxes): Here's a simple function that produces a contour in the shape of a flower with some petals. Note that the function has smaller values inside the shape so we choose a `-INF` bound for the isovalue.
// f = function (x, y, petals=5)
// sin(petals*atan2(y,x)) + norm([x,y]);
// contour(f, isovalue=[-INF,3], bounding_box=8.1);
// Example(2D,NoAxes): If we instead use a `+INF` bound then we get the bounding box with the flower shape removed.
// f = function (x, y, petals=5)
// sin(petals*atan2(y,x)) + norm([x,y]);
// contour(f, isovalue=[3,INF], bounding_box=8.1);
// Example(3D,NoAxes): We can take the previous function a step further and make the isovalue range bounded on both ends, resulting in a hollow shell shape. The nature of the function causes the thickness to vary, which is different from the constant thickness you would get if you subtracted an `offset()` polygon from the inside. Here we extrude this polygon with a twist.
// f = function (x, y, petals=5)
// sin(petals*atan2(y,x)) + norm([x,y]);
// linear_extrude(6, twist=30, scale=0.75, slices=10)
// contour(f, isovalue=[2,3], bounding_box=8.1);
// Example(2D,NoAxes): Another function that needs an isovalue range to create a solid polygon. Increasing the minimum value results in holes in the object.
// f = function(x,y) (x^2+y-11)^2 + (x+y^2-7)^2;
// contour(f, bounding_box=12, isovalue=[0,125]);
// Example(2D,NoAxes): The shape of these contours are somewhat sensitive to pixel size.
// f = function(x,y) x^2+y^2 + 10*(1-cos(360*x)-cos(360*y));
// contour(f, bounding_box=13, isovalue=[-INF,35],
// pixel_size=0.25);
// Example(2D,NoAxes,VPD=1920): An infinite periodic pattern showing contours at one elevation in red, overlaid with a transparent render of the 3D heightmap generated by the function.
// f = function(x,y) 100*(sin(x)*sin(y) * sin(x+y));
// pixel_size = 20;
// isovalue = 1;
// bbox = 720;
// up(isovalue) color("red") linear_extrude(1)
// contour(f, [isovalue,INF], bbox, pixel_size);
// %heightfield(size=[720,720], data = [
// for (y=[-360:pixel_size/2:360]) [
// for(x=[-360:pixel_size/2:360])
// f(x,y)
// ]
// ],
// bottom=-70, maxz=70, style="quincunx");
// Example(2D,NoAxes): A [Cassini oval](https://en.wikipedia.org/wiki/Cassini_oval) is a curve drawn such that for any point on the perimeter, the product of the distances from two fixed points is constant. The curve resembles two circular [metaballs](#functionmodule-metaballs2d) interacting. When the ratio `b/a=1`, there is a cusp where two contours meet at the origin, although the contour algorithm doesn't allow the two contours to touch.
// a=4; b=4.1;
// f = function(x,y) (x^2+y^2)^2 - 2*a^2*(x^2-y^2) + a^4;
// contour(f,bounding_box=[[-6,-3],[6,3]], isovalue=[-INF,b^4]);
module contour(f, isovalue, bounding_box, pixel_size, pixel_count=undef, use_centers=true, smoothing=undef, exact_bounds=false, cp="centroid", anchor="origin", spin=0, atype="hull", show_stats=false, _mball=false) {
module contour(f, isovalue, bounding_box, pixel_size, pixel_count=undef, use_centers=true, smoothing=undef, exact_bounds=false, cp="centroid", anchor="origin", spin=0, atype="hull", show_stats=false, show_box=false, _mball=false) {
pathlist = contour(f, isovalue, bounding_box, pixel_size, pixel_count, use_centers, smoothing, true, exact_bounds, show_stats, _mball);
assert(len(pathlist)>0, "\nNo contour lines found! Cannot generate polygon. Check your isovalue.")
attachable(anchor, spin, two_d=true, region=pathlist, extent=atype=="hull", cp=cp) {
region(pathlist, anchor=anchor, spin=spin, cp=cp, atype=atype);
children();
}
if(show_box)
let(
bbox0 = is_num(bounding_box)
? let(hb=0.5*bounding_box) [[-hb,-hb],[hb,hb]]
: bounding_box,
autopixsize = is_def(pixel_size) ? pixel_size : _getautopixsize(bbox0, default(pixel_count,32^2)),
pixsize = _mball ? pixel_size : _getpixsize(autopixsize, bbox0, exact_bounds),
bbox = _mball ? bounding_box : _getbbox2d(pixsize, bbox0, exact_bounds, f)
) %translate([bbox[0][0],bbox[0][1],-0.05]) linear_extrude(0.1) square(bbox[1]-bbox[0]);
}
function contour(f, isovalue, bounding_box, pixel_size, pixel_count=undef, use_centers=true, smoothing=undef, closed=true, exact_bounds=false, show_stats=false, _mball=false) =
assert(all_defined([f, isovalue]), "\nThe sparameters f and isovalue must both be defined.")
assert(all_defined([f, isovalue]), "\nThe parameters f and isovalue must both be defined.")
assert(is_function(f) ||
(is_list(f) &&
// _mball=true allows pixel_size and bounding_box to coexist with f as array, because metaballs2d() already calculated them
@@ -3576,7 +3703,13 @@ function contour(f, isovalue, bounding_box, pixel_size, pixel_count=undef, use_c
)
)
, "\nWhen f is an array, either bounding_box or pixel_size is required (but not both).")
assert(is_list(isovalue) && len(isovalue)==2 && is_num(isovalue[0]) && is_num(isovalue[1]),
"\nThe isovalue parameter must be a list of two numbers")
let(
isovalmin = isovalue[0],
isovalmax = isovalue[1],
dumiso1 = assert(isovalmin < isovalmax, str("\nBad isovalue range (", isovalmin, ", >= ", isovalmax, "), should be expressed as [min_value, max_value].")),
dumiso2 = assert(isovalmin != -INF || isovalmax != INF, "\nIsovalue range must be finite on one end."),
exactbounds = is_def(exact_bounds) ? exact_bounds : is_list(f),
smoothpasses = is_undef(smoothing) ? ((is_list(use_centers) || use_centers==true) ? 2 : 0) : abs(smoothing),
// new pixel or bounding box centered around original, to fit whole pixels
@@ -3590,14 +3723,15 @@ function contour(f, isovalue, bounding_box, pixel_size, pixel_count=undef, use_c
// proceed with isosurface computations
pixels = _contour_pixels(pixsize, bbox,
fieldarray=is_function(f)?undef:f, fieldfunc=is_function(f)?f:undef,
pixcenters=use_centers, isovalue=isovalue, closed=closed),
segtable = is_list(use_centers) || use_centers ? _MTriSegmentTable : _MSquareSegmentTable,
pathlist = _contour_vertices(pixels, pixsize, isovalue, segtable),
pixcenters=use_centers, isovalmin=isovalmin, isovalmax=isovalmax, closed=closed),
segtablemin = is_list(use_centers) || use_centers ? _MTriSegmentTable : _MSquareSegmentTable,
segtablemax = is_list(use_centers) || use_centers ? _MTriSegmentTable_reverse : _MSquareSegmentTable_reverse,
pathlist = _contour_vertices(pixels, pixsize, isovalmin, isovalmax, segtablemin, segtablemax),
region = _assemble_partial_paths(pathlist, closed),
smoothregion = _region_smooth(region, smoothpasses, bbox),
finalregion = closed ? smoothregion
: [for(p=smoothregion) _pathpts_on_bbox(p, bbox)>1 ? p : concat(p, [p[0]])],
dum2 = show_stats ? _showstats_contour(pixsize, bbox, isovalue, pixels, finalregion) : 0
dum2 = show_stats ? _showstats_contour(pixsize, bbox, isovalmin, isovalmax, pixels, finalregion) : 0
) finalregion;
@@ -3673,7 +3807,7 @@ function _getbbox2d(pixel_size, bounding_box, exactbounds, f=undef) =
/// _showstats_contour() (Private function) - called by contour()
/// Display statistics about a contour region
function _showstats_contour(pixelsize, bbox, isoval, pixels, pathlist) = let(
function _showstats_contour(pixelsize, bbox, isovalmin, isovalmax, pixels, pathlist) = let(
v = column(pixels, 0), // extract pixel vertices
x = column(v,0), // extract x values
y = column(v,1), // extract y values
@@ -3683,7 +3817,7 @@ function _showstats_contour(pixelsize, bbox, isoval, pixels, pathlist) = let(
ymax = max(y)+pixelsize.y,
npts = sum([for(p=pathlist) len(p)]),
npix = len(pixels)
) echo(str("\nContour statistics:\n Isovalue = ", isoval, "\n Pixel size = ", pixelsize,
) echo(str("\nContour statistics:\n Isovalue = ", [isovalmin,isovalmax], "\n Pixel size = ", pixelsize,
"\n Pixels found containing surface = ", npix, "\n Total path vertices = ", npts,
"\n Pixel bounding box for all data = ", bbox,
"\n Pixel bounding box for contour = ", [[xmin,ymin], [xmax,ymax]],

View File

@@ -1246,5 +1246,62 @@ function _assemble_path_fragments(fragments, eps=EPSILON, _finished=[]) =
);
/// Different but similar path assembly function that is much faster than
/// _assemble_path_fragments and can work in 3d, but cannot handle loops.
///
/// Takes a list of paths that are in the correct direction and assembles
/// them into a list of paths. Returns a list of assembled paths.
/// If closed is false then any paths that are closed will have duplicate
/// endpoints, and open paths will not have duplicate endpoints.
/// If closed=true then all paths are assumed closed and none of the returned
/// paths will have duplicate endpoints.
///
/// It is assumed that the paths do not intersect each other.
/// Paths can be in any dimension
function _assemble_partial_paths(paths, closed=false, eps=1e-7) =
let(
pathlist = _assemble_partial_paths_recur(paths, eps)
//// this eliminates crossing paths that cross only at vertices in the input paths lists
// splitpaths =
// [for(path=pathlist) each
// let(
// searchlist = vector_search(path,eps,path),
// duplist = [for(i=idx(searchlist)) if (len(searchlist[i])>1) i]
// )
// duplist==[] ? [path]
// :
// let(
// fragments = [for(i=idx(duplist)) select(path, duplist[i], select(duplist,i+1))]
// )
// len(fragments)==1 ? fragments
// : _assemble_path_fragments(fragments)
// ]
)
closed ? [for(path=pathlist) list_unwrap(path)] : pathlist;
function _assemble_partial_paths_recur(edges, eps, paths=[], i=0) =
i==len(edges) ? paths :
norm(edges[i][0]-last(edges[i]))<eps ? _assemble_partial_paths_recur(edges, eps, paths,i+1) :
let( // Find paths that connects on left side and right side of the edges (if one exists)
left = [for(j=idx(paths)) if (approx(last(paths[j]),edges[i][0],eps)) j],
right = [for(j=idx(paths)) if (approx(last(edges[i]),paths[j][0],eps)) j]
)
let(
keep_path = list_remove(paths,[if (len(left)>0) left[0],if (len(right)>0) right[0]]),
update_path = left==[] && right==[] ? edges[i]
: left==[] ? concat(list_head(edges[i]),paths[right[0]])
: right==[] ? concat(paths[left[0]],slice(edges[i],1,-1))
: left[0] != right[0] ? concat(paths[left[0]],slice(edges[i],1,-2), paths[right[0]])
: concat(paths[left[0]], slice(edges[i],1,-1)) // last arg -2 removes duplicate endpoints but this is handled in passthrough function
)
_assemble_partial_paths_recur(edges, eps, concat(keep_path, [update_path]), i+1);
// vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap

View File

@@ -1173,6 +1173,7 @@ function _filter_region_parts(region1, region2, keep, eps=EPSILON) =
);
function _list_three(a,b,c) =
is_undef(b) ? a :
[

View File

@@ -4004,9 +4004,9 @@ function _prism_fillet_prism(name, basepoly, bot, top, d, k, N, overlap, uniform
// Because of how {{join_prism()}} works, the prism will always make a joint to the shape, but it may be in the wrong location
// when the anchor point is not on the surface, something that may be particularly puzzling with CENTER anchors.
// .
// If you want to shift the prism away from the anchor point you can do that using the `shift1` and `shift2` paramters.
// If you want to shift the prism away from the anchor point you can do that using the `shift1` and `shift2` parameters.
// For anchoring to a flat face, the shift is a 2-vector where the y direction corresponds to the direction of the anchor's spin.
// For a cylinder or extrusion the shift must be a scalar and shifts along the axis. For sphere shift is not permitted.
// For a cylinder or extrusion the shift must be a scalar and shifts along the axis. For spheres, shift is not permitted.
// .
// You can rotate the prism by applying {{zrot()}} to your profile, but the `spin_align` option will enable you to rotate it
// relative to the spin directions of the two descriptions you supply. If you set `spin_align=1` then the Y direction of the
@@ -4065,29 +4065,26 @@ function _prism_fillet_prism(name, basepoly, bot, top, d, k, N, overlap, uniform
// Example(3D,NoAxes,VPT=[11.5254,0.539284,6.44131],VPR=[71.8,0,29.2],VPD=113.4): A circular prism connects a prismoid to a sphere. Note different fillet sizes at each length.
// circ = circle(r=3, $fn=48);
// prismoid(20,13,shift=[-2,1],h=15) let(prism=parent())
// right(30) zrot(20) yrot(12) spheroid(r=10,circum=true,$fn=48) let(ball=parent())
// right(30) zrot(20)spheroid(r=10,circum=true,$fn=48) let(ball=parent())
// prism_connector(circ,prism,RIGHT,ball,LEFT,fillet1=4,fillet2=1);
// Example(3D,NoAxes,VPT=[17.1074,4.56034,8.8345],VPR=[71.8,0,29.2],VPD=126): Here we attach a rounded triangular prism to a prismoid on the left and a regular prism (vnf geometry type) on the right. Note that the point of the triangle which is on the Y axis is aligned with the spin direction on the prismoid, which is the first object.
// $fn=32;
// tri = subdivide_path(round_corners([[-3,-2],[0,5],[3,-2]], cut=1), n=32);
// Example(3D,NoAxes,VPT=[17.1074,4.56034,8.8345],VPR=[71.8,0,29.2],VPD=126): Here we attach a rounded rectangular prism to a prismoid on the left and a regular prism (vnf geometry type) on the right. Note that the long direction of the rectangle which was is the Y axis in the profile specification is aligned with the spin direction of the first object, the prismoid. This is the default alignment for the prism and is equivalent to `spin_align=1`. Note also that we can get away with having long rectangle sides that are not subdivided because the mating surfaces are flat.
// bar = rect([1,12],rounding=.4, $fn=32);
// prismoid(20,15,h=19) let(p1=parent())
// back(15)right(33)zrot(62)xrot(37)zrot(0) regular_prism(n=5, h=30, side=13) let(p2=parent())
// prism_connector(tri, p1, RIGHT, p2, FACE(2), fillet=3);
// Example(3D,NoAxes,VPT=[17.1074,4.56034,8.8345],VPR=[71.8,0,29.2],VPD=126): Here is the same example with `spin_align=2` which aligns the connecting prism on the second object
// $fn=32;
// tri = subdivide_path(round_corners([[-3,-2],[0,5],[3,-2]], cut=1), n=32);
// prism_connector(bar, p1, RIGHT, p2, FACE(2), fillet=1);
// Example(3D,NoAxes,VPT=[17.1074,4.56034,8.8345],VPR=[71.8,0,29.2],VPD=126): Here is the same example with `spin_align=2` which aligns the connecting prism on the second object. Note how it is now aligned parallel to the sides of the face where it attaches on the second object.
// bar = rect([1,12],rounding=.4, $fn=32);
// prismoid(20,15,h=19) let(p1=parent())
// back(15)right(33)zrot(62)xrot(37)zrot(0) regular_prism(n=5, h=30, side=13) let(p2=parent())
// prism_connector(tri, p1, RIGHT, p2, FACE(2), fillet=3, spin_align=2);
// prism_connector(bar, p1, RIGHT, p2, FACE(2), fillet=1, spin_align=2);
// Example(3D,NoAxes,VPT=[17.1074,4.56034,8.8345],VPR=[71.8,0,29.2],VPD=126): Here the connector prism is aligned midway between the spins on the two described objects using `spin_align=12`.
// $fn=32;
// tri = subdivide_path(round_corners([[-3,-2],[0,5],[3,-2]], cut=1), n=32);
// bar = rect([1,12],rounding=.4, $fn=32);
// prismoid(20,15,h=19) let(p1=parent())
// back(15)right(33)zrot(62)xrot(37)zrot(0) regular_prism(n=5, h=30, side=13) let(p2=parent())
// prism_connector(tri, p1, RIGHT, p2, FACE(2), fillet=3, spin_align=12);
// Example(3D,NoAxes,Med,VPT=[17.2141,0.995544,-0.788367],VPR=[55,0,25],VPD=140): Here we apply a shift on object 1. Since this object has a planar connection surface the shift is a 2-vector. Note that the prism has shifted a little too far and is poking out of the connecting cube. When connecting to a planar surface there is no constraint on the extent of the joined prism. It may be much larger than the object it connects to. Note also that the connection on the sphere has shifted to accomodate the change in direction of the prism. If you shift it just a little bit farther forward (perhaps to fit onto a larger cube) the connection to the sphere will fail because the prism doesn't fully intersect the sphere.
// prism_connector(bar, p1, RIGHT, p2, FACE(2), fillet=3, spin_align=12);
// Example(3D,NoAxes,Med,VPT=[17.2141,0.995544,-0.788367],VPR=[55,0,25],VPD=140): Here we apply a shift on object 1. Since this object has a planar connection surface the shift is a 2-vector. Note that the prism has shifted a little too far and is poking out of the cube it connects to. When connecting to a planar surface there is no constraint on the extent of the connecting prism. It may be much larger than the object it connects to. Note also that the connection on the sphere has shifted to accomodate the change in direction of the prism. If you shift it just a little bit farther forward (perhaps to fit onto a larger cube) the connection to the sphere will fail because the prism doesn't fully intersect the sphere.
// circ = circle(r=3, $fn=64);
// cuboid(25) let(cube=parent())
// cuboid(21) let(cube=parent())
// right(40) spheroid(r=15, circum=true,$fn=32) let(ball=parent()){
// prism_connector(circ,cube,RIGHT, ball, [-1,0,.4], fillet=2);
// %prism_connector(circ,cube,RIGHT, ball, [-1,0,.4], fillet=2,shift1=[-6,0]);
@@ -4097,7 +4094,7 @@ function _prism_fillet_prism(name, basepoly, bot, top, d, k, N, overlap, uniform
// cuboid(25) let(cube=parent())
// right(40) spheroid(r=15, circum=true,$fn=32) let(ball=parent())
// prism_connector(circ,cube,RIGHT, ball, [-1,0,.4], fillet=4, shift1=[0,-6],debug_pos=true);
// Example(3D,Med,NoAxes,VPT=[15.9312,-2.44829,-4.47156],VPR=[55,0,25],VPD=155.556): Here two cylinders are connected using a prism shift a shift at each end. Note that the shift is always along the axis of the cylinder. Note that the prisms are a little above the faceted cylinders, exposing a small edge. Using the circum option to enlarge the cylinders will hide this edge inside the cylinders.
// Example(3D,Big,NoAxes,VPT=[15.9312,-2.44829,-4.47156],VPR=[55,0,25],VPD=155.556): Here two cylinders are connected using a prism shift a shift at each end. Note that the shift is always along the axis of the cylinder. Note that the prisms are a little above the faceted cylinders, exposing a small edge that is visible as a kind of dotted line where the fillet meets the cylinder. Using the circum option to enlarge the cylinders will hide this edge inside the cylinders.
// circ = circle(r=3, $fn=64);
// zrot(-20)
// ycyl(l=40,d=20) let(x=parent())
@@ -4116,7 +4113,7 @@ function _prism_fillet_prism(name, basepoly, bot, top, d, k, N, overlap, uniform
// move([11,-23,14])
// sphere(d=10) let(ball=parent())
// prism_connector(circ,cube,CTR,ball,CTR,fillet=2);
// Example(3D,VPT=[-3.84547,-8.36131,0.0624037],VPR=[71.1,0,15.9],VPD=113.4): You can still apply shifts with CENTER anchors. In this case we shift the two connectors outward so that their fillets don't interfere on the cube. Note that we give shift as a scalar, which is interpreted as shift in just the x direction.
// Example(3D,NoAxes,VPT=[-3.84547,-8.36131,0.0624037],VPR=[71.1,0,15.9],VPD=113.4): You can still apply shifts with CENTER anchors. In this case we shift the two connectors outward so that their fillets don't interfere on the cube. Note that we give shift as a scalar, which is interpreted as shift in just the x direction.
// circ = circle(r=3, $fn=64);
// cuboid([30,10,20]) let(cube=parent())
// fwd(22)
@@ -4155,26 +4152,22 @@ function _prism_fillet_prism(name, basepoly, bot, top, d, k, N, overlap, uniform
// rounded_prism(bottom=shape2,top=move([9,11],shape2), h=37, joint_top=5, joint_bot=5, joint_sides=3, atype="intersect")
// let(second=parent())
// prism_connector(flower, first, CTR, second, CTR, fillet=2);
// Example(3D,Med,VPT=[29.1489,9.51572,-4.02049],VPR=[53.6,0,15.9],VPD=126): Connecting to edges. In this example the triangular prism is aligned with object1, the big cube, so its corner is at the edge.
// $fn=32;
// tri = subdivide_path(round_corners([[-3,-2],[0,5],[3,-2]], cut=1), maxlen=1,closed=true);
// zrot(55)
// Example(3D,NoAxes,Med,VPT=[21.6303,-17.7214,-1.32542],VPR=[25.8,0,344.4],VPD=60.2654): Connecting to edges is possible but problems may arise. In this example the skinny rectangular connector prism connects to edge anchors on the two cubes. By default as usual it aligns its direction with the spin of the first object, so the prism lines up with the edge on the left hand cube. Since the edges are not parallel, the prism does **not** align with the second edge. When aligning a skinny object like this with an edge, artifacts are likely to occur if the connector doesn't align neatly with the edge, as happens on the second object.
// bar = subdivide_path(rect([2,12],rounding=.5, $fn=32),maxlen=.5,closed=true);
// cuboid(30) let(big=parent())
// move([30,-30,-10]) xrot(40) cuboid(20) let(small=parent())
// prism_connector(tri, big,RIGHT+FWD, small, BACK+LEFT, fillet=2);
// Example(3D,Med,VPT=[29.1489,9.51572,-4.02049],VPR=[53.6,0,15.9],VPD=126): Here is the same example again, but with the spin aligned to object2, the small cube. Note now that the corner is **not** aligned on the edge of the big cube. You can't align it with both edges at the same time; that would requires twisting the prism.
// $fn=32;
// tri = subdivide_path(round_corners([[-3,-2],[0,5],[3,-2]], cut=1), maxlen=1,closed=true);
// zrot(55)
// move([35,-30,0]) rot(a=35,v=[-1,1,0]) cuboid(20) let(small=parent())
// prism_connector(bar, big, RIGHT+FWD, small, BACK+LEFT, fillet=1);
// Example(3D,NoAxes,Med,VPT=[21.6303,-17.7214,-1.32542],VPR=[25.8,0,344.4],VPD=60.2654): Here with `spin_align=2` the prism now aligns neatly with the edge on the right hand cube. In this case the artifacts that arise have lead to problems that prevent the shape from rendering in CGAL, but this can be worked around by decreasing `overlap` from its default of 1 to 0.5. When the edges are not parallel, it is impossible to line the prism up with both at the same time: that would require the connecting prism to twist. Setting `spin_align=12` will simply result in the prism aligning with **neither** edge.
// bar = subdivide_path(rect([2,12],rounding=.5, $fn=32),maxlen=.5,closed=true);
// cuboid(30) let(big=parent())
// move([30,-30,-10]) xrot(40) cuboid(20) let(small=parent())
// prism_connector(tri, big,RIGHT+FWD, small, BACK+LEFT, spin_align=2,fillet=2);
// Example(3D,VPR=[80.9,0,55.8],VPT=[11.9865,-12.0228,4.48816],VPD=15.3187): Attaching to edges doesn't always produce a good looking result, and sometimes you may get unexpected errors about the fit of the fillet. If you connect a circular prism to an edge you definitely want an even number of points so the top and bottom can line up with the edge. But you may find that artifacts appear when you increase the point count like in this example below where the prism's joint to the edge has a little groove:
// move([35,-30,0]) rot(a=35,v=[-1,1,0]) cuboid(20) let(small=parent())
// prism_connector(bar, big, RIGHT+FWD, small, BACK+LEFT, fillet=1, spin_align=2, overlap=0.5);
// Example(3D,VPR=[80.9,0,55.8],VPT=[11.9865,-12.0228,4.48816],VPD=15.3187): As noted above, attaching to edges doesn't always produce a good looking result, and sometimes you may get unexpected errors about the fit of the fillet, or failure to render. These things happen because the implementation assumes an approximately smooth shape, which is a bad assumption for edges. You can even get artifacts when shapes appear neatly aligned with the edges. For example, if you connect a circular prism to an edge you definitely want an even number of points so the top and bottom can line up with the edge. But you may find that artifacts appear when you increase the point count like in this example below where the prism's joint to the edge has a little groove:
// circ = circle(r=3, $fn=64);
// cuboid(20) let(edge=parent())
// move([30,-30,-8]) zrot(-45) cuboid(20) let(extra=parent())
// prism_connector(circ, edge, RIGHT+FWD, extra, LEFT, fillet=2);
// Example(3D,VPR=[80.9,0,55.8],VPT=[11.9865,-12.0228,4.48816],VPD=15.3187): The artifact shown above occurs because of normals to the prism that cross the edge at a small angle. You can remove it by either decreasing the number of points (32 works in this case) or as shown below, by stretching out the circle so that the top is flatter, which makes the normals parallel to the edge so they don't cross it. Generally results will be best if you can get a point on the edge and if the top surface is perpendicular to the edge.
// Example(3D,VPR=[80.9,0,55.8],VPT=[11.9865,-12.0228,4.48816],VPD=15.3187): The artifact shown above occurs because of normals to the prism that cross the edge at a small angle. You can remove it by either decreasing the number of points (32 works in this case) or as shown below, by stretching out the circle so that the top is flatter, which makes the normals parallel to the edge so they don't cross it. Generally results will be best if you can get a point on the edge and if the top surface is perpendicular to the edge, or at least angled such that the a normal to the prism does not cross the edge when extended by the fillet distance.
// circ = xscale(1.2,circle(r=3, $fn=64));
// cuboid(20) let(edge=parent())
// move([30,-30,-8]) zrot(-45) cuboid(20) let(extra=parent())
@@ -4268,7 +4261,32 @@ function _prism_fillet_prism(name, basepoly, bot, top, d, k, N, overlap, uniform
// prism_connector(scale(.8,circ),
// select(obj,$idx),BACK,
// next(2,select(obj,$idx+2)), FWD, fillet=4,shift1=shift,debug_pos=false);
// Example(3D,NoAxes,Med,VPT=[15.3147,3.4204,-0.243801],VPR=[78.8,0,352.1],VPD=102.06): Here we create a connected pipe configuration by making the outside assembly of connected cylinders first, and then subtracting the interior assembly. Note that a correction is needed to the shift in order to create uniform pipe walls because the length of the connector changes, so different shifts are needed to keep everything parallel.
// bigpipe_d=15;
// smallpipe_d=9;
// wall=1;
// h=30;
// fillet=3;
// sep=30;
// shift=5;
// shift_fix=wall/(sep-bigpipe_d)*shift;
// $fn=128;
// back_half()
// difference(){
// // Pipe exterior
// cyl(d=bigpipe_d,h=h,circum=true) let(leftpipe=parent())
// right(sep)
// cyl(d=bigpipe_d,h=h,circum=true) let(rightpipe=parent())
// prism_connector(circle(d=smallpipe_d,$fn=48),
// leftpipe, RIGHT, rightpipe, LEFT, fillet=fillet, shift2=shift);
// // Interior that will be removed
// cyl(d=bigpipe_d-2*wall,h=h+1,circum=true) let(leftpipe=parent())
// right(sep)
// cyl(d=bigpipe_d-2*wall,h=h+1,circum=true) let(rightpipe=parent())
// prism_connector(circle(d=smallpipe_d-2*wall,$fn=48),
// leftpipe, RIGHT, rightpipe, LEFT, fillet=fillet,
// shift1=-shift_fix,shift2=shift+shift_fix);
// }
// Get the object type from the specified geometry and anchor point

View File

@@ -260,14 +260,14 @@ function rect(size=1, rounding=0, chamfer=0, atype="box", anchor=CENTER, spin=0,
// Example(2D): Fit to Three Points
// pts = [[50,25], [25,-25], [-10,0]];
// circle(points=pts);
// color("red") move_copies(pts) circle();
// color("red") move_copies(pts) circle(r=1.5,$fn=12);
// Example(2D): Fit Tangent to Inside Corner of Two Segments
// path = [[50,25], [-10,0], [25,-25]];
// circle(corner=path, r=15);
// color("red") stroke(path);
// Example(2D): Called as Function
// path = circle(d=50, anchor=FRONT, spin=45);
// stroke(path);
// stroke(path,closed=true);
function circle(r, d, points, corner, anchor=CENTER, spin=0) =
assert(is_undef(corner) || (is_path(corner,[2]) && len(corner) == 3))
assert(is_undef(points) || is_undef(corner), "Cannot specify both points and corner.")
@@ -377,41 +377,41 @@ module circle(r, d, points, corner, anchor=CENTER, spin=0) {
// r=[10,3];
// ydistribute(7){
// union(){
// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue");
// stroke([ellipse(r=r, $fn=6)],width=0.1,color="red");
// stroke([ellipse(r=r, $fn=100)],width=0.1,color="blue");
// stroke([ellipse(r=r, $fn=6)],width=0.2,color="red");
// }
// union(){
// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue");
// stroke([ellipse(r=r, $fn=6,uniform=true)],width=0.1,color="red");
// stroke([ellipse(r=r, $fn=100)],width=0.1,color="blue");
// stroke([ellipse(r=r, $fn=6,uniform=true)],width=0.2,color="red");
// }
// }
// Example(2D): The realigned hexagons are even more different
// Example(2D,NoAxes): The realigned hexagons are even more different
// r=[10,3];
// ydistribute(7){
// union(){
// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue");
// stroke([ellipse(r=r, $fn=6,realign=true)],width=0.1,color="red");
// stroke([ellipse(r=r, $fn=100)],width=0.1,color="blue");
// stroke([ellipse(r=r, $fn=6,realign=true)],width=0.2,color="red");
// }
// union(){
// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue");
// stroke([ellipse(r=r, $fn=6,realign=true,uniform=true)],width=0.1,color="red");
// stroke([ellipse(r=r, $fn=100)],width=0.1,color="blue");
// stroke([ellipse(r=r, $fn=6,realign=true,uniform=true)],width=0.2,color="red");
// }
// }
// Example(2D): For odd $fn the result may not look very elliptical:
// Example(2D,NoAxes): For odd $fn the result may not look very elliptical:
// r=[10,3];
// ydistribute(7){
// union(){
// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue");
// stroke([ellipse(r=r, $fn=5,realign=false)],width=0.1,color="red");
// stroke([ellipse(r=r, $fn=100)],width=0.1,color="blue");
// stroke([ellipse(r=r, $fn=5,realign=false)],width=0.2,color="red");
// }
// union(){
// stroke([ellipse(r=r, $fn=100)],width=0.05,color="blue");
// stroke([ellipse(r=r, $fn=5,realign=false,uniform=true)],width=0.1,color="red");
// stroke([ellipse(r=r, $fn=100)],width=0.1,color="blue");
// stroke([ellipse(r=r, $fn=5,realign=false,uniform=true)],width=0.2,color="red");
// }
// }
// Example(2D): The same ellipse, turned 90 deg, gives a very different result:
// Example(2D,NoAxes): The same ellipse, turned 90 deg, gives a very different result:
// r=[3,10];
// xdistribute(7){
// xdistribute(9){
// union(){
// stroke([ellipse(r=r, $fn=100)],width=0.1,color="blue");
// stroke([ellipse(r=r, $fn=5,realign=false)],width=0.2,color="red");

View File

@@ -1014,7 +1014,7 @@ function regular_prism(n,
let(
style = default(style,"min_edge"),
tex_depth = default(tex_depth,1),
height = one_defined([l, h, length, height],"l,h,length,height",dflt=1),
height = one_defined([l, h, length, height],"l,h,length,height"),
sc = 1/cos(180/n),
ir1 = u_mul(default(ir1,ir), sc),
ir2 = u_mul(default(ir2,ir), sc),

375
skin.scad
View File

@@ -671,7 +671,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close
// linear_sweep(circle(20), texture=tile,
// tex_size=[10,10],tex_depth=5,
// h=40,convexity=4);
// Example: The same tile from above, turned 90 degrees, creates problems at the ends, because the end cap is not a connected polygon. When the ends are disconnected you may find that some parts of the end cap are missing and spurious polygons included.
// Example: The same tile from above, turned 90 degrees, Note that it has endcaps on the disconnected components. These will not appear of `caps=false`.
// shape = skin([rect(2/5),
// rect(2/3),
// rect(2/5)],
@@ -682,7 +682,7 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close
// linear_sweep(circle(20), texture=tile,
// tex_size=[30,20],tex_depth=15,
// h=40,convexity=4);
// Example: This example shows some endcap polygons missing and a spurious triangle
// Example: This example shows a disconnected component combined with the base component.
// shape = skin([rect(2/5),
// rect(2/3),
// rect(2/5)],
@@ -690,25 +690,38 @@ function skin(profiles, slices, refine=1, method="direct", sampling, caps, close
// slices=0,
// caps=false);
// tile = xscale(.5,move([1/2,1,2/3],xrot(90,shape)));
// doubletile = vnf_join([tile, right(.5,tile)]);
// peak = [[[0,0,0],[1,0,0]],
// [[0,1/2,1/4],[1,1/2,1/4]],
// [[0,1,0],[1,1,0]]];
// peakvnf = vnf_vertex_array(peak,reverse=true);
// doubletile = vnf_join([tile,
// right(.5,tile),
// peakvnf
// ]);
// linear_sweep(circle(20), texture=doubletile,
// tex_size=[45,45],tex_depth=15, h=40);
// Example: You can fix ends for disconnected cases using {{top_half()}} and {{bottom_half()}}
// shape = skin([rect(2/5),
// rect(2/3),
// rect(2/5)],
// z=[0,1/2,1],
// slices=0,
// caps=false);
// tile = move([1/2,1,2/3],xrot(90,shape));
// vnf_polyhedron(
// top_half(
// bottom_half(
// linear_sweep(circle(20), texture=tile,
// tex_size=[30,20],tex_depth=15,
// h=40.2,caps=false),
// z=20),
// z=-20));
// tex_size=[40,20],tex_depth=15, h=40);
// Example(3D,NoAxes,VPT=[0.37913,-2.82647,5.92656],VPR=[99.8,0,9.6],VPD=48.815): Here is a simple basket weave pattern created using a texture. We have removed the back to make the weave easier to see.
// diag_weave_vnf = [
// [[0.2, 0, 0], [0.8, 0, 0], [1, 0.2, 0.5], [1, 0.8, 0.5], [0.7, 0.5, 0.5],
// [0.5, 0.3, 0], [0.2, 0, 0.5], [0.8, 0, 0.5], [1, 0.2, 1], [1, 0.8, 1],
// [0.7, 0.5, 1], [0.5, 0.3, 0.5], [1, 0.2, 0], [1, 0.8, 0], [0.8, 1, 0.5],
// [0.2, 1, 0.5], [0.5, 0.7, 0.5], [0.7, 0.5, 0], [0.8, 1, 1], [0.2, 1, 1],
// [0.5, 0.7, 1], [0.8, 1, 0], [0.2, 1, 0], [0, 0.8, 0.5], [0, 0.2, 0.5],
// [0.3, 0.5, 0.5], [0.5, 0.7, 0], [0, 0.8, 1], [0, 0.2, 1], [0.3, 0.5, 1],
// [0, 0.8, 0], [0, 0.2, 0], [0.3, 0.5, 0], [0.2, 0, 1], [0.8, 0, 1], [0.5, 0.3, 1]],
// [[0, 1, 5], [1, 2, 4, 5], [7, 11, 10, 8], [8, 10, 9], [7, 8, 2, 1], [9, 10, 4, 3],
// [10, 11, 5, 4], [0, 5, 11, 6], [12, 13, 17], [13, 14, 16, 17], [3, 4, 20, 18],
// [18, 20, 19], [3, 18, 14, 13], [19, 20, 16, 15], [20, 4, 17, 16], [12, 17, 4, 2],
// [21, 22, 26], [22, 23, 25, 26], [15, 16, 29, 27], [27, 29, 28], [15, 27, 23, 22],
// [28, 29, 25, 24], [29, 16, 26, 25], [21, 26, 16, 14], [30, 31, 32], [31, 6, 11, 32],
// [24, 25, 35, 33], [33, 35, 34], [24, 33, 6, 31], [34, 35, 11, 7],
// [35, 25, 32, 11], [30, 32, 25, 23]]
// ];
// front_half(y=3){
// cyl(d=14.5,h=1,anchor=BOT,rounding=1/3,$fa=1,$fs=.5);
// linear_sweep(circle(d=12), h=12, scale=1.3, texture=diag_weave_vnf,
// tex_size=[5,5], convexity=12);
// }
module linear_sweep(
region, height, center,
@@ -995,6 +1008,61 @@ function linear_sweep(
// tex_taper=[[0,0], [10,0], [10.1,1], [100,1]],
// style="convex",
// convexity=10);
// Example(3D,NoAxes,Med,VPT=[-2.92656,1.26781,0.102897],VPR=[62.7,0,222.4],VPD=216.381): This VNF tile makes a closed shape and the actual main extrusion is not created.
// shape = skin([rect(2/5),
// rect(2/3),
// rect(2/5)],
// z=[0,1/2,1],
// slices=0,
// caps=false);
// tile = move([0,1/2,2/3],yrot(90,shape));
// path = [for(y=[-30:30]) [ 20-3*(1-cos((y+30)/60*360)),y]];
// rotate_sweep(path, closed=false, texture=tile,
// tex_size=[10,10], tex_depth=5);
// Example(3D,Med,VPT=[1.04269,4.35278,-0.716624],VPR=[98.4,0,43.9],VPD=175.268): Adding the angle parameter cuts off the extrusion. Note how each extruded component is capped.
// shape = skin([rect(2/5),
// rect(2/3),
// rect(2/5)],
// z=[0,1/2,1],
// slices=0,
// caps=false);
// tile = move([0,1/2,2/3],yrot(90,shape));
// path = [for(y=[-30:30]) [ 20-3*(1-cos((y+30)/60*360)),y]];
// rotate_sweep(path, closed=false, texture=tile,
// tex_size=[10,15], tex_depth=5, angle=215);
// Example(3D,NoAxes,Med,VPT=[1.00759,3.89216,-1.27032],VPR=[57.1,0,34.8],VPD=240.423): Turning the texture 90 degrees with `tex_rot` produces a texture that ends at the top and bottom.
// shape = skin([rect(2/5),
// rect(2/3),
// rect(2/5)],
// z=[0,1/2,1],
// slices=0,
// caps=false);
// tile = move([0,1/2,2/3],yrot(90,shape));
// path = [for(y=[-30:30]) [ 20-3*(1-cos((y+30)/60*360)),y]];
// rotate_sweep(path, closed=false, texture=tile, tex_rot=90,
// tex_size=[12,8], tex_depth=9, angle=360);
// Example(3D,Med,NoAxes: A basket weave texture, here only half way around the circle to avoid clutter.
// diag_weave_vnf = [
// [[0.2, 0, 0], [0.8, 0, 0], [1, 0.2, 0.5], [1, 0.8, 0.5], [0.7, 0.5, 0.5],
// [0.5, 0.3, 0], [0.2, 0, 0.5], [0.8, 0, 0.5], [1, 0.2, 1], [1, 0.8, 1],
// [0.7, 0.5, 1], [0.5, 0.3, 0.5], [1, 0.2, 0], [1, 0.8, 0], [0.8, 1, 0.5],
// [0.2, 1, 0.5], [0.5, 0.7, 0.5], [0.7, 0.5, 0], [0.8, 1, 1], [0.2, 1, 1],
// [0.5, 0.7, 1], [0.8, 1, 0], [0.2, 1, 0], [0, 0.8, 0.5], [0, 0.2, 0.5],
// [0.3, 0.5, 0.5], [0.5, 0.7, 0], [0, 0.8, 1], [0, 0.2, 1], [0.3, 0.5, 1],
// [0, 0.8, 0], [0, 0.2, 0], [0.3, 0.5, 0], [0.2, 0, 1], [0.8, 0, 1], [0.5, 0.3, 1]],
// [[0, 1, 5], [1, 2, 4, 5], [7, 11, 10, 8], [8, 10, 9], [7, 8, 2, 1], [9, 10, 4, 3],
// [10, 11, 5, 4], [0, 5, 11, 6], [12, 13, 17], [13, 14, 16, 17], [3, 4, 20, 18],
// [18, 20, 19], [3, 18, 14, 13], [19, 20, 16, 15], [20, 4, 17, 16], [12, 17, 4, 2],
// [21, 22, 26], [22, 23, 25, 26], [15, 16, 29, 27], [27, 29, 28], [15, 27, 23, 22],
// [28, 29, 25, 24], [29, 16, 26, 25], [21, 26, 16, 14], [30, 31, 32], [31, 6, 11, 32],
// [24, 25, 35, 33], [33, 35, 34], [24, 33, 6, 31], [34, 35, 11, 7],
// [35, 25, 32, 11], [30, 32, 25, 23]]
// ];
// path = [for(y=[-30:30]) [ 20-3*(1-cos((y+30)/60*360)),y]];
// down(31)linear_extrude(height=1)arc(r=23,angle=[0,180], wedge=true);
// rotate_sweep(path, closed=false, texture=diag_weave_vnf, angle=180,
// tex_size=[10,10], convexity=12, tex_depth=2);
function rotate_sweep(
shape, angle=360,
@@ -1131,6 +1199,7 @@ module rotate_sweep(
}
// Function&Module: spiral_sweep()
// Synopsis: Sweep a path along a helix.
// SynTags: VNF, Geom
@@ -3976,6 +4045,18 @@ function _textured_linear_sweep(
assert(is_bool(rot) || in_list(rot,[0,90,180,270]))
assert(is_bool(caps) || is_bool_list(caps,2))
let(
transform_pt = function(tileind,tilex,tilez,samples,inset,scale,bases,norms)
let(
pos = (tileind + tilex) * samples, // tileind is which tile, tilex is position in a tile
ind = floor(pos),
frac = pos-ind,
texh = scale<0 ? -(1-tilez - inset) * scale
: (tilez - inset) * scale,
base = lerp(bases[ind], select(bases,ind+1), frac),
norm = unit(lerp(norms[ind], select(norms,ind+1), frac))
)
base + norm * texh,
caps = is_bool(caps) ? [caps,caps] : caps,
regions = is_path(region,2)? [[region]] : region_parts(region),
tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture,
@@ -4018,12 +4099,14 @@ function _textured_linear_sweep(
) _vnf_sort_vertices(vnf, idx=[1,0]),
vertzs = !is_vnf(sorted_tile)? undef :
group_sort(sorted_tile[0], idx=1),
tpath = is_vnf(sorted_tile)
? _find_vnf_tile_edge_path(sorted_tile,0)
edge_paths = is_vnf(sorted_tile) ? _tile_edge_path_list(sorted_tile,1) : undef,
tpath = is_def(edge_paths)
? len(edge_paths[0])==0 ? [] : hstack([column(edge_paths[0][0],0), column(edge_paths[0][0],2)])
: let(
row = sorted_tile[0],
rlen = len(row)
) [for (i = [0:1:rlen]) [i/rlen, row[i%rlen]]],
edge_closed_paths = is_def(edge_paths) ? edge_paths[1] : [],
tmat = scale(scale) * zrot(twist) * up(h/2),
pre_skew_vnf = vnf_join([
for (rgn = regions) let(
@@ -4048,13 +4131,7 @@ function _textured_linear_sweep(
for (group = vertzs)
each [
for (vert = group) let(
u = floor((j + vert.x) * samples),
uu = ((j + vert.x) * samples) - u,
texh = tex_scale<0 ? -(1-vert.z - inset) * tex_scale
: (vert.z - inset) * tex_scale,
base = lerp(bases[u], select(bases,u+1), uu),
norm = unit(lerp(norms[u], select(norms,u+1), uu)),
xy = base + norm * texh,
xy = transform_pt(j,vert.x,vert.z,samples, inset, tex_scale, bases, norms),
pt = point3d(xy,vert.y),
v = vert.y / counts.y,
vv = i / counts.y,
@@ -4091,16 +4168,7 @@ function _textured_linear_sweep(
for (j = [0:1:counts.x])
for (tj = [0:1:texcnt.x-1])
if (j != counts.x || tj == 0)
let(
part = (j + (tj/texcnt.x)) * samples,
u = floor(part),
uu = part - u,
texh = tex_scale<0 ? -(1-texture[ti][tj] - inset) * tex_scale
: (texture[ti][tj] - inset) * tex_scale,
base = lerp(bases[u], select(bases,u+1), uu),
norm = unit(lerp(norms[u], select(norms,u+1), uu)),
xy = base + norm * texh
) xy
transform_pt(j, tj/texcnt.x, texture[ti][tj], samples, inset, tex_scale, bases, norms)
])
],
tiles = [
@@ -4134,24 +4202,42 @@ function _textured_linear_sweep(
bases = list_wrap(obases),
norms = list_wrap(onorms),
nupath = [
for (j = [0:1:counts.x-1], vert = tpath) let(
part = (j + vert.x) * samples,
u = floor(part),
uu = part - u,
texh = tex_scale<0 ? -(1-vert.y - inset) * tex_scale
: (vert.y - inset) * tex_scale,
base = lerp(bases[u], select(bases,u+1), uu),
norm = unit(lerp(norms[u], select(norms,u+1), uu)),
xy = base + norm * texh
) xy
for (j = [0:1:counts.x-1], vert = tpath)
transform_pt(j,vert.x,vert.y,samples,inset,tex_scale,bases,norms)
]
) nupath
],
bot_vnf = !caps[0] || brgn==[[]] ? EMPTY_VNF
extra_edge_paths = edge_closed_paths==[] ? []
: [
for (path=rgn)
let(
path = reverse(path),
plen = path_length(path, closed=true),
counts = is_vector(counts,2)? counts :
is_vector(tex_size,2)
? [round(plen/tex_size.x), max(1,round(h/tex_size.y)), ]
: [ceil(6*plen/h), 6],
obases = resample_path(path, n=counts.x * samples, closed=true),
onorms = path_normals(obases, closed=true),
bases = list_wrap(obases),
norms = list_wrap(onorms),
modpaths = [for (j = [0:1:counts.x-1], cpath = edge_closed_paths)
[for(vert = cpath)
transform_pt(j,vert.x,vert.z,samples,inset,tex_scale,bases, norms)]
]
)
each modpaths
],
brgn_empty = [for(item=brgn) if(item!=[]) 1]==[],
bot_vnf = !caps[0] || brgn_empty ? EMPTY_VNF
: vnf_from_region(brgn, down(h/2), reverse=true),
top_vnf = !caps[1] || brgn==[[]] ? EMPTY_VNF
: vnf_from_region(brgn, tmat, reverse=false)
) vnf_join([walls_vnf, bot_vnf, top_vnf])
top_vnf = !caps[1] || brgn_empty ? EMPTY_VNF
: vnf_from_region(brgn, tmat, reverse=false),
extra_vnfs = [
if (caps[0] && len(extra_edge_paths)>0) for(path=extra_edge_paths) [path3d(path,-h/2),[count(len(path))]],
if (caps[1] && len(extra_edge_paths)>0) for(path=extra_edge_paths) [apply(tmat,path3d(path,0)),[count(len(path), reverse=true)]]
]
) vnf_join([walls_vnf, bot_vnf, top_vnf,each extra_vnfs])
]),
skmat = down(h/2) * skew(sxz=shift.x/h, syz=shift.y/h) * up(h/2),
final_vnf = apply(skmat, pre_skew_vnf),
@@ -4165,6 +4251,39 @@ function _textured_linear_sweep(
// Given a VNF texture tile finds the paths on either the x=0 (axis=0) or the y=0 (axis=1) cases.
// Would also find the z=0 paths if you gave axis=2.
//
// It returns two lists, a list of open paths and a list of closed paths. By default a max of
// one open path is permitted; either list can be empty. The paths go in the direction of the segments
// in the VNF.
function _tile_edge_path_list(vnf, axis, maxopen=1) =
let(
verts = vnf[0],
faces = vnf[1],
segs = [for(face=faces, edge=pair(select(verts,face),wrap=true)) if (approx(edge[0][axis],0) && approx(edge[1][axis],0)) [edge[1],edge[0]]],
paths = _assemble_partial_paths(segs),
closedlist = [
for(path=paths)
if (len(path)>3 && approx(path[0],last(path))) list_unwrap(path)
],
openlist = [
for(path=paths)
if (path[0]!=last(path)) path
]
)
assert(len(openlist)<=1, str("VNF has ",len(openlist)," open paths on an edge and at most ",maxopen," is supported."))
[openlist,closedlist];
function _find_vnf_tile_edge_path(vnf, val) =
let(
verts = vnf[0],
@@ -4226,6 +4345,7 @@ function _find_vnf_tile_edge_path(vnf, val) =
/// "hull" = Anchors to the virtual convex hull of the shape.
/// "intersect" = Anchors to the surface of the shape.
function _textured_revolution(
shape, texture, tex_size, tex_scale=1,
inset=false, rot=false, shift=[0,0],
@@ -4301,12 +4421,18 @@ function _textured_revolution(
) zvnf
) _vnf_sort_vertices(utex, idx=[0,1]),
vertzs = is_vnf(texture)? group_sort(tile[0], idx=0) : undef,
bpath = is_vnf(tile)
? _find_vnf_tile_edge_path(tile,1)
edge_paths = is_vnf(tile) ? _tile_edge_path_list(tile,1) : undef,
bpath = is_def(edge_paths)
? len(edge_paths[0])==0 ? [] : hstack([column(edge_paths[0][0],0), column(edge_paths[0][0],2)])
: let(
row = tile[0],
rlen = len(row)
) [for (i = [0:1:rlen]) [i/rlen, row[i%rlen]]],
edge_closed_paths = is_def(edge_paths) ? edge_paths[1] : [],
side_paths = angle==360 || !is_vnf(tile) ? undef
: _tile_edge_path_list(tile,0),
side_open_path = is_undef(side_paths) ? undef : len(side_paths[0])==0 ? [] : side_paths[0][1],
side_closed_paths = is_undef(side_paths) ? [] : side_paths[1],
counts_x = is_vector(counts,2)? counts.x :
is_vector(tex_size,2)
? max(1,round(angle/360*circumf/tex_size.x))
@@ -4322,6 +4448,18 @@ function _textured_revolution(
taperout = [[-1,retaper[0][1]], each retaper, [2,last(retaper)[1]]]
) taperout :
assert(false, "Bad taper= argument value."),
transform_point = function(tileind, tilez, counts_y, bases, norms)
let(
part = tileind * samples,
ind = floor(part),
frac = part - ind,
base = lerp(bases[ind], select(bases,ind+1), frac),
norm = unit(lerp(norms[ind], select(norms,ind+1), frac)),
scale = tex_scale * lookup(tileind/counts_y, taper_lup) * base.x/maxx,
texh = scale<0 ? -(1-tilez - inset) * scale
: (tilez - inset) * scale
)
base - norm * texh,
full_vnf = vnf_join([
for (rgn = regions) let(
rgn_wall_vnf = vnf_join([
@@ -4341,17 +4479,9 @@ function _textured_revolution(
[
[
for (group = vertzs) each [
for (vert = group) let(
part = (j + (1-vert.y)) * samples,
u = floor(part),
uu = part - u,
base = lerp(select(bases,u), select(bases,u+1), uu),
norm = unit(lerp(select(norms,u), select(norms,u+1), uu)),
tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
texh = tex_scale<0 ? -(1-vert.z - inset) * tex_scale * (base.x / maxx)
: (vert.z - inset) * tex_scale * (base.x / maxx),
xyz = base - norm * texh
) zrot(vert.x*angle/counts_x, p=xyz)
for (vert = group)
let(xyz = transform_point(j + (1-vert.y),vert.z,counts_y,bases, norms))
zrot(vert.x*angle/counts_x, p=xyz)
]
],
tile[1]
@@ -4365,20 +4495,11 @@ function _textured_revolution(
let(
v = (j + (tj/texcnt.x)) / counts_x,
mat = zrot(v*angle)
) apply(mat, [
)
apply(mat, [
for (i = [0:1:counts_y-(closed?1:0)], ti = [0:1:texcnt.y-1])
if (i != counts_y || ti == 0)
let(
part = (i + (ti/texcnt.y)) * samples,
u = floor(part),
uu = part - u,
base = lerp(bases[u], select(bases,u+1), uu),
norm = unit(lerp(norms[u], select(norms,u+1), uu)),
tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
texh = tex_scale<0 ? -(1-texture[ti][tj] - inset) * tex_scale * (base.x / maxx)
: (texture[ti][tj] - inset) * tex_scale * (base.x / maxx),
xyz = base - norm * texh
) xyz
transform_point(i + (ti/texcnt.y),texture[ti][tj],counts_y, bases, norms)
])
])
) vnf_vertex_array(
@@ -4391,9 +4512,9 @@ function _textured_revolution(
for (i = [0:1:counts_x-1])
zrot(i*angle/counts_x, rgn_wall_vnf)
]),
endcap_vnf = angle == 360? EMPTY_VNF :
sidecap_vnf = angle == 360? EMPTY_VNF :
let(
cap_rgn = [
cap_rgn = side_open_path == [] ? [] : [
for (path = rgn) let(
plen = path_length(path, closed=closed),
counts_y = is_vector(counts,2)? counts.y :
@@ -4405,35 +4526,16 @@ function _textured_revolution(
ppath = is_vnf(texture)
? [ // VNF tile texture
for (j = [0:1:counts_y-1])
for (group = vertzs, vert = reverse(group))
if (approx(vert.x, 0)) let(
part = (j + (1 - vert.y)) * samples,
u = floor(part),
uu = part - u,
base = lerp(select(bases,u), select(bases,u+1), uu),
norm = unit(lerp(select(norms,u), select(norms,u+1), uu)),
tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
texh = tex_scale<0 ? -(1-vert.z - inset) * tex_scale * (base.x / maxx)
: (vert.z - inset) * tex_scale * (base.x / maxx),
xyz = base - norm * texh
) xyz
//for (group = vertzs, vert = reverse(group))
for(vert=side_open_path)
transform_point(j + (1 - vert.y),vert.z,counts_y,bases, norms)
]
: let( // Heightfield texture
texcnt = [len(texture[0]), len(texture)]
) [
for (i = [0:1:counts_y-(closed?1:0)], ti = [0:1:texcnt.y-1])
if (i != counts_y || ti == 0)
let(
part = (i + (ti/texcnt.y)) * samples,
u = floor(part),
uu = part - u,
base = lerp(bases[u], select(bases,u+1), uu),
norm = unit(lerp(norms[u], select(norms,u+1), uu)),
tex_scale = tex_scale * lookup(part/samples/counts_y, taper_lup),
texh = tex_scale<0 ? -(1-texture[ti][0] - inset) * tex_scale * (base.x / maxx)
: (texture[ti][0] - inset) * tex_scale * (base.x / maxx),
xyz = base - norm * texh
) xyz
transform_point(i + (ti/texcnt.y),texture[ti][0],counts_y,bases, norms)
],
path = closed? ppath : [
[0, ppath[0].y],
@@ -4442,10 +4544,30 @@ function _textured_revolution(
]
) deduplicate(path, closed=closed)
],
vnf2 = vnf_from_region(cap_rgn, xrot(90), reverse=false),
vnf3 = vnf_from_region(cap_rgn, rot([90,0,angle]), reverse=true)
) vnf_join([vnf2, vnf3]),
allcaps_vnf = closed? EMPTY_VNF :
vnf2 = cap_rgn==[] ? EMPTY_VNF : vnf_from_region(cap_rgn, xrot(90), reverse=false),
vnf3 = cap_rgn==[] ? EMPTY_VNF : vnf_from_region(cap_rgn, rot([90,0,angle]), reverse=true),
extra_paths = side_closed_paths==[] ? []
: [for (path = rgn) let(
plen = path_length(path, closed=closed),
counts_y = is_vector(counts,2)? counts.y :
is_vector(tex_size,2)? max(1,round(plen/tex_size.y)) : 6,
obases = resample_path(path, n=counts_y * samples + (closed?0:1), closed=closed),
onorms = path_normals(obases, closed=closed),
bases = closed? list_wrap(obases) : obases,
norms = closed? list_wrap(onorms) : onorms,
modpaths = [for (j = [0:1:counts_y-1], cpath=side_closed_paths)
[for(vert=cpath)
transform_point(j + (1 - vert.y),vert.z,counts_y,bases, norms)]
]
)
each modpaths
],
extra_vnfs = [
if (len(extra_paths)>0) for(path=extra_paths) [xrot(90,path3d(path)), [count(len(path))]],
if (len(extra_paths)>0) for(path=extra_paths) [rot([90,0,angle],p=path3d(path)), [count(len(path),reverse=true)]],
]
) vnf_join([vnf2, vnf3, each extra_vnfs]),
endcaps_vnf = closed? EMPTY_VNF :
let(
plen = path_length(rgn[0], closed=closed),
counts_y = is_vector(counts,2)? counts.y :
@@ -4457,39 +4579,55 @@ function _textured_revolution(
bases = xrot(90, p=path3d(rbases)),
norms = xrot(90, p=path3d(rnorms)),
caps_vnf = vnf_join([
for (j = [-1,0]) let(
for (epath=edge_closed_paths, j = [-1,0])
let(
base = select(bases,j),
norm = unit(select(norms,j)),
ppath = [
for (vert = epath) let(
uang = vert.x / counts_x,
tex_scale = tex_scale * lookup(j+1, taper_lup),
texh = tex_scale<0 ? -(1-vert.z - inset) * tex_scale * (base.x / maxx)
: (vert.z - inset) * tex_scale * (base.x / maxx),
xyz = base - norm * texh
) zrot(angle*uang, p=xyz)
],
faces = [count(ppath,reverse=j==0)]
)
for(i=[0:1:counts_x-1])
[zrot(i*angle/counts_x, ppath), faces],
if (len(bpath)>0)
for (j = [-1,0])
let(
base = select(bases,j),
norm = unit(select(norms,j)),
ppath = [
for (vert = bpath) let(
uang = vert.x / counts_x,
tex_scale = tex_scale * lookup([0,1][j+1], taper_lup),
tex_scale = tex_scale * lookup(j+1, taper_lup),
texh = tex_scale<0 ? -(1-vert.y - inset) * tex_scale * (base.x / maxx)
: (vert.y - inset) * tex_scale * (base.x / maxx),
xyz = base - norm * texh
) zrot(angle*uang, p=xyz)
],
pplen = len(ppath),
zed = j<0? max(column(ppath,2)) :
min(column(ppath,2)),
zed = j<0? max(column(ppath,2)): min(column(ppath,2)),
slice_vnf = [
[
each ppath,
[0, 0, zed],
], [
for (i = [0:1:pplen-2])
j<0? [pplen, i, (i+1)%pplen] :
[pplen, (i+1)%pplen, i]
j<0? [pplen, i, (i+1)%pplen]
: [pplen, (i+1)%pplen, i]
]
],
cap_vnf = vnf_join([
]
)
for (i = [0:1:counts_x-1])
zrot(i*angle/counts_x, p=slice_vnf)
])
) cap_vnf
])
) caps_vnf
) vnf_join([walls_vnf, endcap_vnf, allcaps_vnf])
) vnf_join([walls_vnf, sidecap_vnf, endcaps_vnf])
]),
skmat = zrot(start) * down(-miny) * skew(sxz=shift.x/h, syz=shift.y/h) * up(-miny),
skvnf = apply(skmat, full_vnf),
@@ -4499,6 +4637,7 @@ function _textured_revolution(
) reorient(anchor,spin,orient, geom=geom, p=skvnf);
module _textured_revolution(
shape, texture, tex_size, tex_scale=1,
inset=false, rot=false, shift=[0,0],

View File

@@ -34,8 +34,8 @@ include <coords.scad>
include <geometry.scad>
include <regions.scad>
include <strings.scad>
include <skin.scad>
include <vnf.scad>
include <skin.scad>
include <utility.scad>
include <partitions.scad>
include <structs.scad>

View File

@@ -1092,8 +1092,8 @@ function _split_polygon_at_x(poly, x) =
out1 = [for (p = poly2) if(p.x <= x) p],
out2 = [for (p = poly2) if(p.x >= x) p],
out3 = [
if (len(out1)>=3) each split_path_at_self_crossings(out1),
if (len(out2)>=3) each split_path_at_self_crossings(out2),
if (len(out1)>=3 && polygon_area(out1)>EPSILON) each split_path_at_self_crossings(out1),
if (len(out2)>=3 && polygon_area(out2)>EPSILON) each split_path_at_self_crossings(out2),
],
out = [for (p=out3) if (len(p) > 2) list_unwrap(p)]
) out;