textured sweep fix & contour isovalue change

This commit is contained in:
Adrian Mariano 2025-04-02 20:20:13 -04:00
parent bdcc440bb7
commit 10132e5780
5 changed files with 224 additions and 151 deletions

View file

@ -3784,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);
@ -5093,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())
@ -5140,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)
@ -5200,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")

View file

@ -1238,46 +1238,6 @@ function _contour_vertices(pxlist, pxsize, isovalmin, isovalmax, segtablemin, se
];
function _assemble_partial_paths(paths, closed=false, eps=1e-7) =
let(
pathlist = _assemble_partial_paths_recur(paths, eps) /*,
// 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, 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);
/// ---------- 3D metaball stuff starts here ----------
@ -3158,43 +3118,32 @@ function _metaballs2dfield(funclist, transmatrix, bbox, pixsize, nballs) = let(
// <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 can be specified as either a range or a single value; for a height field, the
// contour isovalue is its elevation. A single contour isovalue is equivalent to the range `[isovalue,INF]`.
// .
// 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, the isothis 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.
// .
// The previous paragraph also applies to contours: the isovalue range can also be finite or unbounded at one
// end, with either `c_min=-INF` or `c_max=INF`. A scalar isovalue is equivalent to the range `[isovalue,INF]`.
// The returned polygon is the set of points `[x,y]` that satisfy `c_min <= f(x,y) <= c_max`. If `f(x,y)`
// has values larger than `c_min` and values smaller than `c_max`, then the result is a polygon bounded by two
// paths corresponding to the contours at `c_min` and `c_max`, as well as being clipped by the bounding box.
// Setting isovalue to `[-INF,c_max]` or `[c_min,INF]` always produces a polygon with a single bounding contour,
// which itself can be unbounded (and truncated by the bounding box). If you find that your contour appears as
// a hole inside a solid rectangle, think about whether the function values inside the polygon are smaller or
// larger than your isovalue. If the values inside are smaller, use `isovalue=[-INF,c_max]`. If the values
// inside are larger, you get a bounded object using a scalar `isovalue=c_min` or the range `isovalue=[c_min,INF]`.
// .
// **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.
@ -3209,9 +3158,12 @@ 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
// 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.
// .
@ -3598,18 +3550,18 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// .
// ***Closed and unclosed paths***
// .
// The functional form of `contour()` 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. The module form of
// `contour()` always closes the polygons at the bounding box edges.
// resulting path list is a valid region with no duplicated vertices in any path.
// .
// 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.
// 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 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]`. A scalar isovalue is equivalent to the range `[isovalue,INF]`.
@ -3641,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,
@ -3659,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,
@ -3676,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]) [
@ -3684,14 +3636,14 @@ 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. However, because the contour by default encloses *higher* values, and this function has higher values outside of the contour, we get a picture of the bounding box with a flower-shaped hole in it.
// 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=2, bounding_box=8.1);
// Example(2D,NoAxes): Because the function in the previous example has higher values outside the contour, we specify a range for the isovalue instead of a scalar. Then the contour surrounds the values inside that range. A scalar `isovalue=3` is equivalent to the range `[3,INF]`. Instead, we force the upper end of the range to be bounded using the range `[-INF,3]`, which gives us a solid polygon flower.
// contour(f, isovalue=[-INF,2], 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=[-INF,3], bounding_box=8.1);
// 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]);
@ -3710,7 +3662,7 @@ function _showstats_isosurface(voxsize, bbox, isoval, cubes, triangles, faces) =
// isovalue = 1;
// bbox = 720;
// up(isovalue) color("red") linear_extrude(1)
// contour(f, isovalue, bbox, pixel_size);
// 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])
@ -3742,7 +3694,7 @@ module contour(f, isovalue, bounding_box, pixel_size, pixel_count=undef, use_cen
}
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
@ -3751,9 +3703,11 @@ 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 = is_list(isovalue) ? isovalue[0] : isovalue,
isovalmax = is_list(isovalue) ? isovalue[1] : INF,
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),

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 :
[

165
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)]);
// 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));
// 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=[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,
@ -3976,6 +3989,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 +4043,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 +4075,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 +4112,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 +4146,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 +4195,33 @@ function _textured_linear_sweep(
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],