Merge pull request #1636 from amatulic/anachronist_vnf

Deprecated thickness in vnf_sheet and bezier_sheet
This commit is contained in:
adrianVmariano 2025-04-16 06:18:11 -04:00 committed by GitHub
commit eecd3e023d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 127 additions and 121 deletions

View File

@ -398,8 +398,8 @@ function bezier_length(bezier, start_u=0, end_u=1, max_deflect=0.01) =
// bezier = The list of control points that define a 2D Bezier curve.
// line = a list of two distinct 2d points defining a line
function bezier_line_intersection(bezier, line) =
assert(is_path(bezier,2), "The input ´bezier´ must be a 2d bezier")
assert(_valid_line(line,2), "The input `line` is not a valid 2d line")
assert(is_path(bezier,2), "\nThe input 'bezier' must be a 2d bezier.")
assert(_valid_line(line,2), "\nThe input 'line' is not a valid 2d line.")
let(
a = _bezier_matrix(len(bezier)-1)*bezier, // bezier algebraic coeffs.
n = [-line[1].y+line[0].y, line[1].x-line[0].x], // line normal
@ -469,7 +469,7 @@ function bezpath_curve(bezpath, splinesteps=16, N=3, endpoint=true) =
assert(is_path(bezpath))
assert(is_int(N))
assert(is_int(splinesteps) && splinesteps>0)
assert(len(bezpath)%N == 1, str("A degree ",N," bezier path should have a multiple of ",N," points in it, plus 1."))
assert(len(bezpath)%N == 1, str("\nA degree ",N," bezier path should have a multiple of ",N," points in it, plus 1."))
let(
segs = (len(bezpath)-1) / N,
step = 1 / splinesteps,
@ -510,7 +510,7 @@ function bezpath_closest_point(bezpath, pt, N=3, max_err=0.01, seg=0, min_seg=un
assert(is_vector(pt))
assert(is_int(N))
assert(is_num(max_err))
assert(len(bezpath)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1."))
assert(len(bezpath)%N == 1, str("\nA degree ",N," bezier path should have a multiple of ",N," points in it, plus 1."))
let(curve = select(bezpath,seg*N,(seg+1)*N))
(seg*N+1 >= len(bezpath))? (
let(curve = select(bezpath, min_seg*N, (min_seg+1)*N))
@ -544,7 +544,7 @@ function bezpath_closest_point(bezpath, pt, N=3, max_err=0.01, seg=0, min_seg=un
function bezpath_length(bezpath, N=3, max_deflect=0.001) =
assert(is_int(N))
assert(is_num(max_deflect))
assert(len(bezpath)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1."))
assert(len(bezpath)%N == 1, str("\nA degree ",N," bezier path should have a multiple of ",N," points in it, plus 1."))
sum([
for (seg=[0:1:(len(bezpath)-1)/N-1]) (
bezier_length(
@ -590,29 +590,29 @@ function path_to_bezpath(path, closed, tangents, uniform=false, size, relsize) =
let(closed=default(closed,false))
assert(is_bool(closed))
assert(is_bool(uniform))
assert(num_defined([size,relsize])<=1, "Can't define both size and relsize")
assert(is_path(path,[2,3]),"Input path is not a valid 2d or 3d path")
assert(is_undef(tangents) || is_path(tangents,[2,3]),"Tangents must be a 2d or 3d path")
assert(is_undef(tangents) || len(path)==len(tangents), "Input tangents must be the same length as the input path")
assert(num_defined([size,relsize])<=1, "\nCan't define both size and relsize.")
assert(is_path(path,[2,3]),"\nInput path is not a valid 2d or 3d path.")
assert(is_undef(tangents) || is_path(tangents,[2,3]),"\nTangents must be a 2d or 3d path.")
assert(is_undef(tangents) || len(path)==len(tangents), "\nInput tangents must be the same length as the input path.")
let(
curvesize = first_defined([size,relsize,0.1]),
relative = is_undef(size),
lastpt = len(path) - (closed?0:1)
)
assert(is_num(curvesize) || len(curvesize)==lastpt, str("Size or relsize must have length ",lastpt))
assert(is_num(curvesize) || len(curvesize)==lastpt, str("\nSize or relsize must have length ",lastpt,"."))
let(
sizevect = is_num(curvesize) ? repeat(curvesize, lastpt) : curvesize,
tangents = is_def(tangents) ? [for(t=tangents) let(n=norm(t)) assert(!approx(n,0),"Zero tangent vector") t/n] :
tangents = is_def(tangents) ? [for(t=tangents) let(n=norm(t)) assert(!approx(n,0),"\nZero tangent vector.") t/n] :
path_tangents(path, uniform=uniform, closed=closed)
)
assert(min(sizevect)>0, "Size and relsize must be greater than zero")
assert(min(sizevect)>0, "\nSize and relsize must be greater than zero.")
[
for(i=[0:1:lastpt-1])
let(
first = path[i],
second = select(path,i+1),
seglength = norm(second-first),
dummy = assert(seglength>0, str("Path segment has zero length from index ",i," to ",i+1)),
dummy = assert(seglength>0, str("\nPath segment has zero length from index ",i," to ",i+1,".")),
segdir = (second-first)/seglength,
tangent1 = tangents[i],
tangent2 = -select(tangents,i+1), // Need this to point backward, in direction of the curve
@ -673,16 +673,16 @@ function path_to_bezcornerpath(path, closed, size, relsize) =
is_1region(path) ? path_to_bezcornerpath(path[0], default(closed,true), tangents, size, relsize) :
let(closed=default(closed,false))
assert(is_bool(closed))
assert(num_defined([size,relsize])<=1, "Can't define both size and relsize")
assert(is_path(path,[2,3]),"Input path is not a valid 2d or 3d path")
assert(num_defined([size,relsize])<=1, "\nCan't define both size and relsize.")
assert(is_path(path,[2,3]),"\nInput path is not a valid 2d or 3d path.")
let(
curvesize = first_defined([size,relsize,0.5]),
relative = is_undef(size),
pathlen = len(path)
)
assert(is_num(curvesize) || len(curvesize)==pathlen, str("Size or relsize must have length ",pathlen))
assert(is_num(curvesize) || len(curvesize)==pathlen, str("\nSize or relsize must have length ",pathlen,"."))
let(sizevect = is_num(curvesize) ? repeat(curvesize, pathlen) : curvesize)
assert(min(sizevect)>0, "Size or relsize must be greater than zero")
assert(min(sizevect)>0, "\nSize or relsize must be greater than zero.")
let(
roundpath = closed ? [
for(i=[0:pathlen-1]) let(p3=select(path,[i-1:i+1]))
@ -783,9 +783,9 @@ is_collinear(p)
// closed = bezpath_close_to_axis(bez, axis="Y");
// debug_bezier(closed);
function bezpath_close_to_axis(bezpath, axis="X", N=3) =
assert(is_path(bezpath,2), "bezpath_close_to_axis() works only on 2D bezier paths.")
assert(is_path(bezpath,2), "\nbezpath_close_to_axis() works only on 2D bezier paths.")
assert(is_int(N))
assert(len(bezpath)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1."))
assert(len(bezpath)%N == 1, str("\nA degree ",N," bezier path should have a multiple of ",N," points in it, plus 1."))
let(
sp = bezpath[0],
ep = last(bezpath)
@ -827,9 +827,9 @@ function bezpath_close_to_axis(bezpath, axis="X", N=3) =
// debug_bezier(closed);
function bezpath_offset(offset, bezier, N=3) =
assert(is_vector(offset,2))
assert(is_path(bezier,2), "bezpath_offset() works only on 2D bezier paths.")
assert(is_path(bezier,2), "\nbezpath_offset() works only on 2D bezier paths.")
assert(is_int(N))
assert(len(bezier)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1."))
assert(len(bezier)%N == 1, str("\nA degree ",N," bezier path should have a multiple of ",N," points in it, plus 1."))
let(
backbez = reverse([ for (pt = bezier) pt+offset ]),
bezend = len(bezier)-1
@ -912,7 +912,7 @@ function bez_begin(pt,a,r,p) =
assert(len(pt)==3 || is_undef(p))
is_vector(a)? [pt, pt+(is_undef(r)? a : r*unit(a))] :
is_finite(a)? [pt, pt+spherical_to_xyz(r,a,default(p,90))] :
assert(false, "Bad arguments.");
assert(false, "\nBad arguments.");
// Function: bez_tang()
@ -948,7 +948,7 @@ function bez_tang(pt,a,r1,r2,p) =
pt,
pt+spherical_to_xyz(r2,a,p)
] :
assert(false, "Bad arguments.");
assert(false, "\nBad arguments.");
// Function: bez_joint()
@ -984,11 +984,11 @@ function bez_joint(pt,a1,a2,r1,r2,p1,p2) =
) [
if (is_vector(a1)) (pt+r1*unit(a1))
else if (is_finite(a1)) (pt+spherical_to_xyz(r1,a1,p1))
else assert(false, "Bad Arguments"),
else assert(false, "\nBad arguments."),
pt,
if (is_vector(a2)) (pt+r2*unit(a2))
else if (is_finite(a2)) (pt+spherical_to_xyz(r2,a2,p2))
else assert(false, "Bad Arguments")
else assert(false, "\nBad arguments.")
];
@ -1012,7 +1012,7 @@ function bez_end(pt,a,r,p) =
assert(len(pt)==3 || is_undef(p))
is_vector(a)? [pt+(is_undef(r)? a : r*unit(a)), pt] :
is_finite(a)? [pt+spherical_to_xyz(r,a,default(p,90)), pt] :
assert(false, "Bad arguments.");
assert(false, "\nBad arguments.");
@ -1119,8 +1119,8 @@ function bezier_patch_reverse(patch) =
// pts = bezier_patch_points(patch, [0:0.2:1], [0:0.2:1]);
// for (row=pts) move_copies(row) color("magenta") sphere(d=3, $fn=12);
function bezier_patch_points(patch, u, v) =
assert(is_range(u) || is_vector(u) || is_finite(u), "Input u is invalid")
assert(is_range(v) || is_vector(v) || is_finite(v), "Input v is invalid")
assert(is_range(u) || is_vector(u) || is_finite(u), "\nInput u is invalid.")
assert(is_range(v) || is_vector(v) || is_finite(v), "\nInput v is invalid.")
!is_num(u) && !is_num(v) ?
let(
vbezes = [for (i = idx(patch[0])) bezier_points(column(patch,i), u)]
@ -1259,12 +1259,12 @@ function bezier_vnf(patches=[], splinesteps=16, style="default") =
assert(all_positive(splinesteps))
let(splinesteps = force_list(splinesteps,2))
is_bezier_patch(patches)? _bezier_rectangle(patches, splinesteps=splinesteps,style=style)
: assert(is_list(patches),"Invalid patch list")
: assert(is_list(patches),"\nInvalid patch list.")
vnf_join(
[
for (patch=patches)
is_bezier_patch(patch)? _bezier_rectangle(patch, splinesteps=splinesteps,style=style)
: assert(false,"Invalid patch list")
: assert(false,"\nInvalid patch list.")
]
);
@ -1373,8 +1373,8 @@ function bezier_vnf(patches=[], splinesteps=16, style="default") =
// color("red")move_copies(flatten(patch)) sphere(r=0.3,$fn=9);
function bezier_vnf_degenerate_patch(patch, splinesteps=16, reverse=false, return_edges=false) =
!return_edges ? bezier_vnf_degenerate_patch(patch, splinesteps, reverse, true)[0] :
assert(is_bezier_patch(patch), "Input is not a Bezier patch")
assert(is_int(splinesteps) && splinesteps>0, "splinesteps must be a positive integer")
assert(is_bezier_patch(patch), "\nInput is not a Bezier patch.")
assert(is_int(splinesteps) && splinesteps>0, "\nsplinesteps must be a positive integer.")
let(
row_degen = [for(row=patch) all_equal(row,eps=EPSILON)],
col_degen = [for(col=transpose(patch)) all_equal(col,eps=EPSILON)],
@ -1547,8 +1547,8 @@ function bezier_vnf_degenerate_patch(patch, splinesteps=16, reverse=false, retur
// endcap1="dot",endcap2="arrow2",color="blue");
function bezier_patch_normals(patch, u, v) =
assert(is_range(u) || is_vector(u) || is_finite(u), "Input u is invalid")
assert(is_range(v) || is_vector(v) || is_finite(v), "Input v is invalid")
assert(is_range(u) || is_vector(u) || is_finite(u), "\nInput u is invalid.")
assert(is_range(v) || is_vector(v) || is_finite(v), "\nInput v is invalid.")
!is_num(u) && !is_num(v) ?
let(
vbezes = [for (i = idx(patch[0])) bezier_points(column(patch,i), u)],
@ -1573,29 +1573,30 @@ function bezier_patch_normals(patch, u, v) =
// Topics: Bezier Patches
// See Also: bezier_patch_normals(), vnf_sheet()
// Usage:
// vnf = bezier_sheet(patch, thickness, [splinesteps=], [balanced=]
// vnf = bezier_sheet(patch, delta, [splinesteps=], [style=]);
// Description:
// Constructs a thin sheet from a bezier patch by offsetting the given patch along the normal vectors
// to the patch surface. The thickness value must be small enough so that no points cross each other
// when the offset is computed, because that results in invalid geometry and gives rendering errors.
// to the patch surface.
// The `delta` parameter is a 2-vector specifying the offset distances for both surfaces that form the
// final sheet. The values for each offset must be small enough so that no points cross each other
// when the offset is computed, because that results in invalid geometry and rendering errors.
// Rendering errors may not manifest until you add other objects to your model.
// **It is your responsibility to avoid invalid geometry!**
// .
// The normals are computed using {{bezier_patch_normals()}} and if they are degenerate then
// the computation fails or produces incorrect results. See {{bezier_patch_normals()}} for
// examples of various ways the normals can be degenerate.
// Once the offset surfaces from the bezier patch are computed, they are connected by filling
// in the boundary strips between them.
// .
// When thickness is positive, the given bezier patch is extended toward its "inside", which is the
// side that appears purple in the "thrown together" view. You can extend the patch in the other direction
// using a negative thickness value.
// A negative offset value extends the patch toward its "inside", which is the side that appears purple
// in the "thrown together" view when the patch is viewed by itself. Extending only toward the inside with a delta of `[0,-value]` or
// `[-value,0]` (the order doesn't matter) means that your original bezier patch surface remains unchanged in the output.
// Both offset surfaces may be extended in the same direction as long as the offset values are different.
// Arguments:
// patch = bezier patch to process
// thickness = amount to offset; can be positive or negative
// delta = a 2-vector specifying two different offsets from the bezier patch, in any order. Positive values offset the patch from its "exterior" side, and negative values offset from the "interior" side.
// ---
// splinesteps = Number of segments on the border edges of the bezier surface. You can specify [USTEPS,VSTEPS]. Default: 16
// balanced = if true, then offset the bezier surface by half the specified thickness on each side. This increases execution time because two offsets must be performed. The sign of `thickness` does not matter. Default: false
// style = {{vnf_vertex_array()}} style to use. Default: "default"
// Example(3D): In this case, a positive thickness extends downward from that side of the surface.
// Example(3D): A negative delta extends downward from the "inside" surface of the bezier patch, leaving the original bezier patch unchanged on the top surface.
// patch = [
// // u=0,v=0 u=1,v=0
// [[-50,-50, 0], [-16,-50, 20], [ 16,-50, -20], [50,-50, 0]],
@ -1604,8 +1605,8 @@ function bezier_patch_normals(patch, u, v) =
// [[-50, 50, 0], [-16, 50, -20], [ 16, 50, 20], [50, 50, 0]],
// // u=0,v=1 u=1,v=1
// ];
// vnf_polyhedron(bezier_sheet(patch, 10));
// Example(3D): Using the previous example, setting `balanced=true` causes half the specified thickness to be extended from each side of the sheet. This is somewhat slower because it requires two separate offset operations. The original bezier patch is shown solid, with the thicker sheet shown transparent.
// vnf_polyhedron(bezier_sheet(patch, [0,-10]));
// Example(3D): Using the previous example, setting two positive offsets results in a sheet above the original bezier patch. The original bezier patch is shown in green for comparison.
// patch = [
// // u=0,v=0 u=1,v=0
// [[-50,-50, 0], [-16,-50, 20], [ 16,-50, -20], [50,-50, 0]],
@ -1614,25 +1615,27 @@ function bezier_patch_normals(patch, u, v) =
// [[-50, 50, 0], [-16, 50, -20], [ 16, 50, 20], [50, 50, 0]],
// // u=0,v=1 u=1,v=1
// ];
// vnf_polyhedron(bezier_vnf(patch));
// %vnf_polyhedron(bezier_sheet(patch, 10, balanced=true));
// color("lime") vnf_polyhedron(bezier_vnf(patch));
// vnf_polyhedron(bezier_sheet(patch, [10,15]));
function bezier_sheet(patch, thickness, splinesteps=16, style="default", balanced=false) =
function bezier_sheet(patch, delta, splinesteps=16, style="default", thickness=undef) =
assert(is_bezier_patch(patch))
assert(all_nonzero([thickness]), "thickness must be nonzero")
assert(is_num(delta) || is_vector(delta,2,zero=false), "\ndelta must be a 2-vector designating two different offset distances.")
let(
dumwarn = is_def(thickness) || is_num(delta) ? echo("\nThe 'thickness' parameter is deprecated and has been replaced by 'delta'. Use the range [0,-thickness] or [-thickness,0] to reproduce the former behavior.") : 0,
del = is_def(thickness) ? [0,-thickness] : is_num(delta) ? [0,-delta] : delta,
splinesteps = force_list(splinesteps,2),
uvals = lerpn(0,1,splinesteps.x+1),
vvals = lerpn(1,0,splinesteps.y+1),
pts = bezier_patch_points(patch, uvals, vvals),
normals = bezier_patch_normals(patch, uvals, vvals),
dummy=assert(is_matrix(flatten(normals)),"Bezier patch has degenerate normals"),
offset0 = balanced ? pts - 0.5*thickness*normals : pts,
offset1 = pts + (balanced ? 0.5 : 1) * thickness*normals,
dummy=assert(is_matrix(flatten(normals)),"\nBezier patch has degenerate normals."),
offset0 = pts - del[0]*normals,
offset1 = pts - del[1]*normals,
allpoints = [for(i=idx(offset0)) concat(offset0[i], reverse(offset1[i]))],
vnf = vnf_vertex_array(allpoints, col_wrap=true, caps=true, style=style)
)
thickness<0 ? vnf_reverse_faces(vnf) : vnf;
del[0]<del[1] ? vnf_reverse_faces(vnf) : vnf;
@ -1680,7 +1683,7 @@ module debug_bezier(bezpath, width=1, N=3) {
check =
assert(is_path(bezpath),"bezpath must be a path")
assert(is_int(N) && N>0, "N must be a positive integer")
assert(len(bezpath)%N == 1, str("A degree ",N," bezier path shound have a multiple of ",N," points in it, plus 1."));
assert(len(bezpath)%N == 1, str("A degree ",N," bezier path should have a multiple of ",N," points in it, plus 1."));
$fn=8;
stroke(bezpath_curve(bezpath, N=N), width=width, color="cyan");
color("green")

131
vnf.scad
View File

@ -82,8 +82,8 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces.
// triangulate = If true, triangulates endcaps to resolve possible CGAL issues. This can be an expensive operation if the endcaps are complex. Default: false
// convexity = (module) Max number of times a line could intersect a wall of the shape.
// texture = A texture name string, or a rectangular array of scalar height values (0.0 to 1.0), or a VNF tile that defines the texture to apply to vertical surfaces. See {{texture()}} for what named textures are supported.
// tex_size = An optional 2D target size for the textures at `points[0][0]`. Actual texture sizes are scaled somewhat to evenly fit the available surface. Default: `[5,5]`
// tex_reps = If given instead of tex_size, a 2-vector giving the number of texture tile repetitions in the horizontal and vertical directions.
// tex_size = An optional 2D target size (scalar or 2-vector) for the textures at `points[0][0]`. This size is approximate; the actual texture sizes are scaled as needed for whole tiles to fit the available surface. Default: `[5,5]`
// tex_reps = If given instead of tex_size, an integer scalar or 2-vector giving the number of texture tile repetitions in the horizontal and vertical directions.
// tex_inset = If numeric, lowers the texture into the surface by the specified proportion, e.g. 0.5 would lower it half way into the surface. If `true`, insets by exactly its full depth. Default: `false`
// tex_rot = Rotate texture by specified angle, which must be a multiple of 90 degrees. Default: 0
// tex_depth = Specify texture depth; if negative, invert the texture. Default: 1.
@ -206,7 +206,7 @@ EMPTY_VNF = [[],[]]; // The standard empty VNF with no vertices or faces.
// apply(m, [ [rgroove[0].x,0,-z], each rgroove, [last(rgroove).x,0,-z] ])
// ], caps=true, col_wrap=true, reverse=true);
// vnf_polyhedron(vnf, convexity=8);
// Example(3D,Med,NoAxes,VPD=300,VPT=[48,48,0]): When applying a texture to a vertex array, remember that the density of the texture follows the density of the vertex array grid. Here is a sheet with a wrinkle in both x and y directions, using location data generated by {{smooth_path()}}. The bezier curves have non-uniformly distributed spline points, indicated by the red dots along each edge. This results in a non-uniform distribution of the texture tiling.
// Example(3D,Med,NoAxes,VPD=300,VPT=[48,48,0]): When applying a texture to a vertex array, remember that the density of the texture follows the density of the vertex array grid. Here is a surface with a wrinkle in both x and y directions, using location data generated by {{smooth_path()}}. The bezier curves have non-uniformly distributed spline points, indicated by the red dots along each edge. This results in a non-uniform distribution of the texture tiling.
// include <BOSL2/rounding.scad>
//
// xprofile = smooth_path([[0,0,0], [25,0,0], [49,0,-10], [51,0,10], [75,0,0], [100,0,0]],
@ -312,8 +312,8 @@ function vnf_vertex_array(
tex_depth=1, tex_extra, tex_skip, sidecaps,sidecap1,sidecap2, normals
) =
assert(in_list(style,["default","alt","quincunx", "convex","concave", "min_edge","min_area","flip1","flip2"]))
assert(is_matrix(points[0], n=3),"Point array has the wrong shape or points are not 3d")
assert(is_consistent(points), "Non-rectangular or invalid point array")
assert(is_matrix(points[0], n=3),"\nPoint array has the wrong shape or points are not 3d.")
assert(is_consistent(points), "\nNon-rectangular or invalid point array.")
assert(is_bool(triangulate))
is_def(texture) ?
_textured_point_array(points=points, texture=texture, tex_reps=tex_reps, tex_size=tex_size,
@ -321,8 +321,8 @@ function vnf_vertex_array(
col_wrap=col_wrap, row_wrap=row_wrap, tex_depth=tex_depth, caps=caps, cap1=cap1, cap2=cap2, reverse=reverse,
style=style, tex_extra=tex_extra, tex_skip=tex_skip, sidecaps=sidecaps, sidecap1=sidecap1, sidecap2=sidecap2,normals=normals,triangulate=triangulate)
:
assert(!(any([caps,cap1,cap2]) && !col_wrap), "col_wrap must be true if caps are requested (without texture)")
assert(!(any([caps,cap1,cap2]) && row_wrap), "Cannot combine caps with row_wrap (without texture)")
assert(!(any([caps,cap1,cap2]) && !col_wrap), "\ncol_wrap must be true if caps are requested (without texture).")
assert(!(any([caps,cap1,cap2]) && row_wrap), "\nCannot combine caps with row_wrap (without texture).")
let(
pts = flatten(points),
pcnt = len(pts),
@ -520,7 +520,7 @@ function vnf_tri_array(
row_wrap=false,
reverse=false
) =
assert(!(any([caps,cap1,cap2]) && row_wrap), "Cannot combine caps with row_wrap")
assert(!(any([caps,cap1,cap2]) && row_wrap), "\nCannot combine caps with row_wrap.")
let(
plen = len(points),
// append first vertex of each polygon to its end if wrapping columns
@ -675,7 +675,7 @@ function _lofttri(p1, p2, i1offset, i2offset, n1, n2, reverse=false, trilist=[],
// text3d("Invalid",size=1,anchor=CENTER,
// orient=FRONT,h=.1);
function vnf_join(vnfs) =
assert(is_vnf_list(vnfs) , "Input must be a list of VNFs")
assert(is_vnf_list(vnfs) , "\nInput must be a list of VNFs.")
len(vnfs)==1 ? vnfs[0]
:
let (
@ -688,7 +688,7 @@ function vnf_join(vnfs) =
if ( len(face) >= 3 )
[ for (j = face)
assert( j>=0 && j<len(vnfs[i][0]),
str("VNF number ", i, " has a face indexing an nonexistent vertex") )
str("\nVNF number ", i, " has a face indexing an nonexistent vertex.") )
offs[i] + j ]
]
)
@ -730,13 +730,13 @@ function vnf_join(vnfs) =
// vnf_polyhedron(vnf);
function vnf_from_polygons(polygons,fast=false,eps=EPSILON) =
assert(is_list(polygons) && is_path(polygons[0]),"Input should be a list of polygons")
assert(is_list(polygons) && is_path(polygons[0]),"\nInput should be a list of polygons.")
let(
offs = cumsum([0, for(p=polygons) len(p)]),
faces = [for(i=idx(polygons))
let(
area=fast ? 1 : polygon_area(polygons[i]),
dummy=assert(is_def(area) || is_collinear(polygons[i],eps=eps),str("Polygon ", i, " is not coplanar"))
dummy=assert(is_def(area) || is_collinear(polygons[i],eps=eps),str("\nPolygon ", i, " is not coplanar."))
)
if (is_def(area) && area > eps)
[for (j=idx(polygons[i])) offs[i]+j]
@ -920,7 +920,7 @@ function vnf_from_region(region, transform, reverse=false, triangulate=true) =
let(
cleaved = path3d(_cleave_connected_region(rgn))
)
assert( cleaved, "The region is invalid")
assert( cleaved, "\nThe region is invalid.")
let(
face = is_undef(transform)? cleaved : apply(transform,cleaved),
faceidxs = reverse? [for (i=[len(face)-1:-1:0]) i] : [for (i=[0:1:len(face)-1]) i]
@ -1101,7 +1101,7 @@ function vnf_triangulate(vnf) =
faces = [for (face=vnf[1])
each (len(face)==3 ? [face] :
let( tris = polygon_triangulate(verts, face) )
assert( tris!=undef, "Some `vnf` face cannot be triangulated.")
assert( tris!=undef, "\nSome VNF face cannot be triangulated.")
tris ) ]
)
[verts, faces];
@ -1279,8 +1279,8 @@ function _split_2dpolygons_at_each_x(polys, xs, _i=0) =
/// dir_ind = slice direction, 0=X, 1=Y, or 2=Z
/// cuts = A list of scalar values for locating the cuts
function _slice_3dpolygons(polys, dir, cuts) =
assert( [for (poly=polys) if (!is_path(poly,3)) 1] == [], "Expects list of 3D paths.")
assert( is_vector(cuts), "The split list must be a vector.")
assert( [for (poly=polys) if (!is_path(poly,3)) 1] == [], "\nExpected list of 3D paths.")
assert( is_vector(cuts), "\nThe split list must be a vector.")
assert( in_list(dir, ["X", "Y", "Z"]))
let(
I = ident(3),
@ -1291,7 +1291,7 @@ function _slice_3dpolygons(polys, dir, cuts) =
if (polygon_area(poly)>EPSILON) // Discard zero area polygons
let(
plane = plane_from_polygon(poly,1e-4))
assert(plane,"Found non-coplanar face.")
assert(plane,"\nFound non-coplanar face.")
let(
normal = point3d(plane),
pnormal = normal - (normal*I[dir_ind])*I[dir_ind]
@ -1344,7 +1344,7 @@ function _slice_3dpolygons(polys, dir, cuts) =
// "origin" = Anchor at the origin, oriented UP.
module vnf_polyhedron(vnf, convexity=2, cp="centroid", anchor="origin", spin=0, orient=UP, atype="hull") {
vnf = is_vnf_list(vnf)? vnf_join(vnf) : vnf;
assert(in_list(atype, _ANCHOR_TYPES), "Anchor type must be \"hull\" or \"intersect\"");
assert(in_list(atype, _ANCHOR_TYPES), "\nAnchor type must be \"hull\" or \"intersect\".");
attachable(anchor,spin,orient, vnf=vnf, extent=atype=="hull", cp=cp) {
polyhedron(vnf[0], vnf[1], convexity=convexity);
children();
@ -1450,7 +1450,7 @@ function vnf_area(vnf) =
/// The centroid of a tetrahedron is the average of its vertices.
/// The centroid of the total is the volume weighted average.
function _vnf_centroid(vnf,eps=EPSILON) =
assert(is_vnf(vnf) && len(vnf[0])!=0 && len(vnf[1])!=0,"Invalid or empty VNF given to centroid")
assert(is_vnf(vnf) && len(vnf[0])!=0 && len(vnf[1])!=0,"\nInvalid or empty VNF given to centroid.")
let(
verts = vnf[0],
pos = sum([
@ -1463,7 +1463,7 @@ function _vnf_centroid(vnf,eps=EPSILON) =
[ vol, (v0+v1+v2)*vol ]
])
)
assert(!approx(pos[0],0, eps), "The vnf has self-intersections.")
assert(!approx(pos[0],0, eps), "\nThe vnf has self-intersections.")
pos[1]/pos[0]/4;
// Function: vnf_bounds()
@ -1483,7 +1483,7 @@ function _vnf_centroid(vnf,eps=EPSILON) =
// Example:
// echo(vnf_bounds(cube([2,3,4],center=true))); // Displays [[-1, -1.5, -2], [1, 1.5, 2]]
function vnf_bounds(vnf,fast=false) =
assert(is_vnf(vnf), "Invalid VNF")
assert(is_vnf(vnf), "\nInvalid VNF.")
fast ? pointlist_bounds(vnf[0])
: let(
vert = vnf[0]
@ -1661,8 +1661,8 @@ function projection(vnf,cut=false,eps=EPSILON) =
// vnf_polyhedron(cutvnf);
// stroke(boundary,color="red");
function vnf_halfspace(plane, vnf, closed=true, boundary=false) =
assert(_valid_plane(plane), "Invalid plane")
assert(is_vnf(vnf), "Invalid vnf")
assert(_valid_plane(plane), "\nInvalid plane.")
assert(is_vnf(vnf), "\nInvalid VNF.")
let(
inside = [for(x=vnf[0]) plane*[each x,-1] >= -EPSILON ? 1 : 0],
vertexmap = [0,each cumsum(inside)],
@ -1673,7 +1673,7 @@ function vnf_halfspace(plane, vnf, closed=true, boundary=false) =
: let(
allpaths = _assemble_paths(newvert, faces_edges_vertices[1]),
newpaths = [for(p=allpaths) if (len(p)>=3) p
else assert(approx(p[0],p[1]),"Orphan edge found when assembling cut edges.")
else assert(approx(p[0],p[1]),"\nOrphan edge found when assembling cut edges.")
]
)
boundary ? [[newvert, faces_edges_vertices[0]], newpaths]
@ -1716,7 +1716,7 @@ function _vnfcut(plane, vertices, vertexmap, inside, faces, vertcount, newfaces=
first = search([[1,0]],pair(pts_inside,wrap=true),0)[0],
second = search([[0,1]],pair(pts_inside,wrap=true),0)[0]
)
assert(len(first)==1 && len(second)==1, "Found concave face in VNF. Run vnf_triangulate first to ensure convex faces.")
assert(len(first)==1 && len(second)==1, "\nFound concave face in VNF. Run vnf_triangulate first to ensure convex faces.")
let(
newface = [each select(vertexmap,select(faces[i],second[0]+1,first[0])),vertcount, vertcount+1],
newvert = [plane_line_intersection(plane, select(vertices,select(faces[i],first[0],first[0]+1)),eps=0),
@ -1855,8 +1855,8 @@ function vnf_bend(vnf,r,d,axis="Z") =
)
let(
span_chk = axis=="Z"?
assert(bmin.y > 0 || bmax.y < 0, "Entire shape MUST be completely in front of or behind y=0.") :
assert(bmin.z > 0 || bmax.z < 0, "Entire shape MUST be completely above or below z=0."),
assert(bmin.y > 0 || bmax.y < 0, "\nEntire shape MUST be completely in front of or behind y=0.") :
assert(bmin.z > 0 || bmax.z < 0, "\nEntire shape MUST be completely above or below z=0."),
steps = 1+ceil(segs(r) * (extent[1]-extent[0])/(2*PI*r)),
step = (extent[1]-extent[0]) / steps,
bend_at = [for(i = [1:1:steps-1]) i*step+extent[0]],
@ -1911,7 +1911,7 @@ function vnf_bend(vnf,r,d,axis="Z") =
// vnf = torus(d_maj=4, d_min=4);
// vnf_hull(vnf);
function vnf_hull(vnf) =
assert(is_vnf(vnf) || is_path(vnf,3),"Input must be a VNF or a 3d path")
assert(is_vnf(vnf) || is_path(vnf,3),"\nInput must be a VNF or a 3d path.")
let(
pts = is_vnf(vnf) ? select(vnf[0],unique(flatten(vnf[1])))
: vnf,
@ -1985,7 +1985,7 @@ function _sort_pairs0(arr) =
// boundary = vnf_boundary(vnf_merge_points(cutvnf));
// stroke(boundary,color="green");
function vnf_boundary(vnf,merge=true,idx=false) =
assert(!idx || !merge, "Cannot request indices unless marge=false and VNF contains no duplicate vertices")
assert(!idx || !merge, "\nCannot request indices unless marge=false and VNF contains no duplicate vertices.")
let(
vnf = merge ? vnf_merge_points(vnf) : vnf,
edgelist= [ for(face=vnf[1], edge=pair(face,wrap=true))
@ -2074,37 +2074,35 @@ function vnf_small_offset(vnf, delta, merge=true) =
// Topics: VNF Manipulation
// See Also: vnf_small_offset(), vnf_boundary(), vnf_merge_points()
// Usage:
// newvnf = vnf_sheet(vnf, thickness, [style=], [merge=], [balanced=]);
// newvnf = vnf_sheet(vnf, delta, [style=], [merge=]);
// Description:
// Constructs a thin sheet from a vnf by offsetting the vnf along the normal vectors estimated at
// each vertex by averaging the normals of the adjacent faces. This is done using {{vnf_small_offset()}.
// The thickness value must be small enough so that no points cross each other
// The `delta` parameter is a 2-vector specifying the offset distances for both surfaces that form the
// final sheet. The values for each offset must be small enough so that no points cross each other
// when the offset is computed, because that results in invalid geometry and rendering errors.
// Rendering errors may not manifest until you add other objects to your model.
// **It is your responsibility to avoid invalid geometry!**
// .
// Once the offset to the original VNF is computed the original and offset VNF are connected by filling
// in the boundary strip(s) between them
// Once the offsets to the original VNFs are computed, they are connected by filling
// in the boundary strips between them.
// .
// When thickness is positive, the given surface is extended toward its "inside", which is the
// side that appears purple in the "thrown together" view. This is the opposite direction
// of {{vnf_small_offset()}}. Extending toward the inside means that your original VNF remains unchanged
// in the output. You can extend the patch in the other direction
// using a negative thickness value. When you extend to the outside with a negative thickness, your VNF needs to have all
// of its faces reversed to produce a valid polyhedron, so your original VNF is reversed in the output.
// A negative offset value extends the surface toward its "inside", which is the side that appears purple
// in the "thrown together" view. Extending only toward the inside with a delta of `[0,-value]` or
// `[-value,0]` (the order doesn't matter) means that your original VNF remains unchanged in the output.
// Both offset surfaces may be extended in the same direction as long as the offset values are different.
// .
// **The input VNF must not contain duplicate points.** By default, vnf_sheet() calls {{vnf_merge_points()}}
// to remove duplicate points. Note, however, that this operation can be slow. If you are **certain** there are no duplicate points you can
// set `merge=false` to disable the automatic point merge and save time. The result of running on a VNF with duplicate points is likely to
// be incorrect or invalid, or it may result in cryptic errors.
// to remove duplicate points, although this operation can be slow. If you are **certain** there are no
// duplicate points, you can set `merge=false` to disable the automatic point merge and save time. The
// result of running on a VNF with duplicate points is likely to be incorrect or invalid, or it may result in cryptic errors.
// Arguments:
// vnf = vnf to process
// thickness = thickness of sheet to produce; can be positive or negative
// delta = a 2-vector specifying two different offsets from the original VNF, in any order. Positive values offset the VNF from its "exterior" side, and negative values offset from the "interior" side.
// ---
// style = {{vnf_vertex_array()}} style to use. Default: "default"
// merge = if false then do not run {{vnf_merge_points()}}. Default: true
// balanced = if true, then offset the input surface by half the specified thickness on each side. This increases execution time because two offsets must be performed. The sign of `thickness` does not matter. Default: false
// Example(3D): In this example, the top of the surface is "interior", so a negative thickness extends that side upward.
// merge = If false, then do not run {{vnf_merge_points()}}. Default: true
// Example(3D,VPD=350,VPR=[60,0,40],VPT=[0,107,15]): In this example, the top of the surface is "interior", so a negative thickness extends that side upward, preserving the "exterior" side of the surface at the bottom.
// pts = [
// for(x=[30:5:180]) [
// for(y=[-6:0.5:6])
@ -2112,19 +2110,19 @@ function vnf_small_offset(vnf, delta, merge=true) =
// ]
// ];
// vnf=vnf_vertex_array(pts);
// vnf_polyhedron(vnf_sheet(vnf,-10));
// Example(3D): Same as previous example, but with `balanced=true` to offset both sides of the surface equally by half the specified thickness. The output is shown transparent with the original surface inside.
// vnf_polyhedron(vnf_sheet(vnf,[-10,0]));
// Example(3D,ThrownTogether=true,VPD=350,VPR=[60,0,40],VPT=[0,107,15]): Same as previous example, but with both sides offset equally. The offset order doesn't matter. The output is shown transparent with the original surface inside. We can also set `merge=false` if we know our original VNF has no duplicate points.
// pts = [
// for(x=[30:5:180]) [
// for(y=[-6:0.5:6])
// [7*y,x, sin(x)*y^2]
// ]
// ];
// vnf=vnf_vertex_array(pts);
// vnf=vnf_vertex_array(pts, reverse=true);
// vnf_polyhedron(vnf);
// %vnf_polyhedron(vnf_sheet(vnf,-10,
// merge=false, balanced=true));
// Example(3D): This example has multiple holes
// %vnf_polyhedron(vnf_sheet(vnf, [-6,6],
// merge=false));
// Example(3D): This example has multiple holes.
// pts = [
// for(x=[-10:2:10]) [
// for(y=[-10:2:10])
@ -2136,24 +2134,29 @@ function vnf_small_offset(vnf, delta, merge=true) =
// [43,42,63,88,108,109,135,
// 134,129,155,156,164,165]);
// newvnf = [vnf[0],newface];
// vnf_polyhedron(vnf_sheet(newvnf,2));
// Example(3D): When applied to a sphere the sheet is constructed inward, so the object appears unchanged, but cutting it in half reveals that we have changed the sphere into a shell.
// vnf_polyhedron(vnf_sheet(newvnf,[2,0]));
// Example(3D,VPD=320): When only a negative offset is applied to a sphere, the sheet is constructed inward, so the object appears unchanged, but cutting it in half reveals that we have changed the sphere into a shell.
// vnf = sphere(d=100, $fn=28);
// left_half()
// vnf_polyhedron(vnf_sheet(vnf,15));
// vnf_polyhedron(vnf_sheet(vnf,[0,-15]));
function vnf_sheet(vnf, thickness, style="default", merge=true, balanced=false) =
function vnf_sheet(vnf, delta, style="default", merge=true, thickness=undef) =
assert(is_num(delta) || is_vector(delta,2,zero=false), "\ndelta must be a 2-vector designating two different offset distances.")
let(
dumwarn = is_def(thickness) || is_num(delta) ? echo("\nThe 'thickness' parameter is deprecated and has been replaced by 'delta'. Use the range [0,-thickness] or [-thickness,0] to reproduce the former behavior.") : 0,
del = is_def(thickness) ? [0,-thickness] : is_num(delta) ? [0,-delta] : delta,
vnf = merge ? vnf_merge_points(vnf) : vnf,
offset0 = balanced ? vnf_small_offset(vnf, thickness/2, merge=false) : vnf,
offset1 = vnf_small_offset(vnf, balanced ? -thickness/2 : -thickness, merge=false),
offset0 = vnf_small_offset(vnf, del[0], merge=false),
offset1 = vnf_small_offset(vnf, del[1], merge=false),
boundary = vnf_boundary(offset0,merge=false,idx=true),
newvnf = vnf_join([offset0,
vnf_reverse_faces(offset1),
for(p=boundary) vnf_vertex_array([select(offset1[0],p),select(offset0[0],p)],col_wrap=true,style=style)
])
newvnf = vnf_join([
offset0,
vnf_reverse_faces(offset1),
for(p=boundary)
vnf_vertex_array([select(offset1[0],p),select(offset0[0],p)],col_wrap=true,style=style)
])
)
thickness < 0 ? vnf_reverse_faces(newvnf) : newvnf;
del[0]>del[1] ? vnf_reverse_faces(newvnf) : newvnf;
@ -2390,7 +2393,7 @@ module debug_vnf(vnf, faces=true, vertices=true, opacity=0.5, size=1, convexity=
// Returns a list of non-manifold errors with the given VNF.
// Each error has the format `[ERR_OR_WARN,CODE,MESG,POINTS,COLOR]`.
function _vnf_validate(vnf, show_warns=true, check_isects=false) =
assert(is_vnf(vnf), "Invalid VNF")
assert(is_vnf(vnf), "\nInvalid VNF.")
let(
varr = vnf[0],
faces = vnf[1],