Compare commits

...

15 commits

Author SHA1 Message Date
Joshua T Corbin
75301c06f8
Merge ae6e830edb into 53af9121e7 2024-08-13 16:18:48 -04:00
Revar Desmera
53af9121e7
Merge pull request #1464 from adrianVmariano/master
doc fix & screws fix
2024-08-11 22:48:27 -07:00
Adrian Mariano
1ab0a85453 doc fixes and screws fix 2024-08-09 21:52:51 -04:00
Revar Desmera
736fad321b
Merge pull request #1459 from adrianVmariano/master 2024-07-27 14:01:30 -07:00
Adrian Mariano
f96e521e9b bugfix for rot_copies: sa parameter was ignored 2024-07-27 10:04:58 -04:00
Adrian Mariano
7c737fd0a3 doc tweaks 2024-07-27 09:36:51 -04:00
Revar Desmera
cc08eb3323
Merge pull request #1458 from adrianVmariano/master
Fix new examples
2024-07-27 02:21:31 -07:00
Adrian Mariano
5375e41af4 example fix 2024-07-26 06:33:25 -04:00
Adrian Mariano
c2b5cd148e add 3D to examples so they display 2024-07-26 06:07:25 -04:00
Revar Desmera
78ea8e4770
Merge pull request #1457 from adrianVmariano/master
vnf_sheet & bezier_sheet
2024-07-25 23:01:31 -07:00
Adrian Mariano
7892d488ba docfix 2024-07-25 21:56:40 -04:00
Adrian Mariano
88e11dfcaf docfix 2024-07-25 21:28:45 -04:00
Adrian Mariano
cff856362b add vnf_sheet, bezier_sheet, vnf_boundary and vnf_thin_offset 2024-07-25 21:04:07 -04:00
Adrian Mariano
0519b5f94c doc tweaks 2024-07-20 11:23:07 -04:00
Joshua T Corbin
ae6e830edb Prune undefined values thru struct_set 2024-04-18 09:02:28 -04:00
8 changed files with 302 additions and 18 deletions

View file

@ -1446,6 +1446,58 @@ function bezier_patch_normals(patch, u, v) =
: column(bezier_patch_normals(patch,u,force_list(v)),0);
// Function: bezier_sheet()
// Synopsis: Creates a thin sheet from a bezier patch by extruding in normal to the patch
// SynTags: VNF
// Topics: Bezier Patches
// See Also: bezier_patch_normals(), vnf_sheet()
// 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 will give 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 will fail or produce incorrect results. See {{bezier_patch_normals()}} for
// examples of various ways the normals can be degenerate.
// .
// When thickness is positive, the given bezier patch is extended towards 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.
// Arguments:
// patch = bezier patch to process
// thickness = amount to offset; can be positive or negative
// ---
// splinesteps = Number of segments on the border edges of the bezier surface. You can specify [USTEPS,VSTEPS]. Default: 16
// style = {{vnf_vertex_array()}} style to use. Default: "default"
// Example(3D):
// patch = [
// // u=0,v=0 u=1,v=0
// [[-50,-50, 0], [-16,-50, 20], [ 16,-50, -20], [50,-50, 0]],
// [[-50,-16, 20], [-16,-16, 20], [ 16,-16, -20], [50,-16, 20]],
// [[-50, 16, 20], [-16, 16, -20], [ 16, 16, 20], [50, 16, 20]],
// [[-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));
function bezier_sheet(patch, thickness, splinesteps=16, style="default") =
assert(is_bezier_patch(patch))
assert(all_nonzero([thickness]), "thickness must be nonzero")
let(
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"),
offset = pts + thickness*normals,
allpoints = [for(i=idx(pts)) concat(pts[i], reverse(offset[i]))],
vnf = vnf_vertex_array(allpoints, col_wrap=true, caps=true, style=style)
)
thickness<0 ? vnf_reverse_faces(vnf) : vnf;
// Section: Debugging Beziers

View file

@ -814,7 +814,7 @@ function grid_copies(spacing, n, size, stagger=false, inside=undef, nonzero, p=_
// n = Optional number of evenly distributed copies, rotated around the axis.
// sa = Starting angle, in degrees. For use with `n`. Angle is in degrees counter-clockwise. Default: 0
// delta = [X,Y,Z] amount to move away from cp before rotating. Makes rings of copies. Default: `[0,0,0]`
// subrot = If false, don't sub-rotate children as they are copied around the ring. Only makes sense when used with `delta`. Default: `true`
// subrot = If false, don't sub-rotate children as they are copied around the ring. Instead maintain their native orientation. The false setting is only allowed when `delta` is given. Default: `true`
// p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
//
// Side Effects:
@ -853,6 +853,7 @@ function grid_copies(spacing, n, size, stagger=false, inside=undef, nonzero, p=_
// color("red",0.333) yrot(90) cylinder(h=20, r1=5, r2=0);
module rot_copies(rots=[], v, cp=[0,0,0], n, sa=0, offset=0, delta=[0,0,0], subrot=true)
{
assert(subrot || norm(delta)>0, "subrot can only be false if delta is not zero");
req_children($children);
sang = sa + offset;
angs = !is_undef(n)?
@ -867,7 +868,7 @@ module rot_copies(rots=[], v, cp=[0,0,0], n, sa=0, offset=0, delta=[0,0,0], subr
translate(cp) {
rotate(a=$ang, v=v) {
translate(delta) {
rot(a=(subrot? sang : $ang), v=v, reverse=true) {
rot(a=subrot? 0 : $ang, v=v, reverse=true) {
translate(-cp) {
children();
}
@ -880,6 +881,7 @@ module rot_copies(rots=[], v, cp=[0,0,0], n, sa=0, offset=0, delta=[0,0,0], subr
function rot_copies(rots=[], v, cp=[0,0,0], n, sa=0, offset=0, delta=[0,0,0], subrot=true, p=_NO_ARG) =
assert(subrot || norm(delta)>0, "subrot can only be false if delta is not zero")
let(
sang = sa + offset,
angs = !is_undef(n)?
@ -893,7 +895,7 @@ function rot_copies(rots=[], v, cp=[0,0,0], n, sa=0, offset=0, delta=[0,0,0], su
translate(cp) *
rot(a=ang, v=v) *
translate(delta) *
rot(a=(subrot? sang : ang), v=v, reverse=true) *
rot(a=subrot? 0 : ang, v=v, reverse=true) *
translate(-cp)
]
)
@ -935,6 +937,7 @@ function rot_copies(rots=[], v, cp=[0,0,0], n, sa=0, offset=0, delta=[0,0,0], su
// sa = Starting angle, in degrees. For use with `n`. Angle is in degrees counter-clockwise from Y+, when facing the origin from X+. First unrotated copy is placed at that angle.
// r = If given, makes a ring of child copies around the X axis, at the given radius. Default: 0
// d = If given, makes a ring of child copies around the X axis, at the given diameter.
// subrot = If false, don't sub-rotate children as they are copied around the ring. Instead maintain their native orientation. The false setting is only allowed when `d` or `r` is given. Default: `true`
// subrot = If false, don't sub-rotate children as they are copied around the ring.
// p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
//
@ -972,12 +975,16 @@ module xrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true)
{
req_children($children);
r = get_radius(r=r, d=d, dflt=0);
assert(all_nonnegative([r]), "d/r must be nonnegative");
assert(subrot || r>0, "subrot can only be false if d or r is given");
rot_copies(rots=rots, v=RIGHT, cp=cp, n=n, sa=sa, delta=[0, r, 0], subrot=subrot) children();
}
function xrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true, p=_NO_ARG) =
let( r = get_radius(r=r, d=d, dflt=0) )
assert(all_nonnegative([r]), "d/r must be nonnegative")
assert(subrot || r>0, "subrot can only be false if d or r is given")
rot_copies(rots=rots, v=RIGHT, cp=cp, n=n, sa=sa, delta=[0, r, 0], subrot=subrot, p=p);
@ -1016,7 +1023,7 @@ function xrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true, p=_NO_ARG)
// sa = Starting angle, in degrees. For use with `n`. Angle is in degrees counter-clockwise from X-, when facing the origin from Y+.
// r = If given, makes a ring of child copies around the Y axis, at the given radius. Default: 0
// d = If given, makes a ring of child copies around the Y axis, at the given diameter.
// subrot = If false, don't sub-rotate children as they are copied around the ring.
// subrot = If false, don't sub-rotate children as they are copied around the ring. Instead maintain their native orientation. The false setting is only allowed when `d` or `r` is given. Default: `true`
// p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
//
// Side Effects:
@ -1053,12 +1060,16 @@ module yrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true)
{
req_children($children);
r = get_radius(r=r, d=d, dflt=0);
assert(all_nonnegative([r]), "d/r must be nonnegative");
assert(subrot || r>0, "subrot can only be false if d or r is given");
rot_copies(rots=rots, v=BACK, cp=cp, n=n, sa=sa, delta=[-r, 0, 0], subrot=subrot) children();
}
function yrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true, p=_NO_ARG) =
let( r = get_radius(r=r, d=d, dflt=0) )
assert(all_nonnegative([r]), "d/r must be nonnegative")
assert(subrot || r>0, "subrot can only be false if d or r is given")
rot_copies(rots=rots, v=BACK, cp=cp, n=n, sa=sa, delta=[-r, 0, 0], subrot=subrot, p=p);
@ -1098,7 +1109,7 @@ function yrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true, p=_NO_ARG)
// sa = Starting angle, in degrees. For use with `n`. Angle is in degrees counter-clockwise from X+, when facing the origin from Z+. Default: 0
// r = If given, makes a ring of child copies around the Z axis, at the given radius. Default: 0
// d = If given, makes a ring of child copies around the Z axis, at the given diameter.
// subrot = If false, don't sub-rotate children as they are copied around the ring. Default: true
// subrot = If false, don't sub-rotate children as they are copied around the ring. Instead maintain their native orientation. The false setting is only allowed when `d` or `r` is given. Default: `true`
// p = Either a point, pointlist, VNF or Bezier patch to be translated when used as a function.
//
// Side Effects:
@ -1133,13 +1144,18 @@ function yrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true, p=_NO_ARG)
// color("red",0.333) yrot(-90) cylinder(h=20, r1=5, r2=0, center=true);
module zrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true)
{
req_children($children);
r = get_radius(r=r, d=d, dflt=0);
assert(all_nonnegative([r]), "d/r must be nonnegative");
assert(subrot || r>0, "subrot can only be false if d or r is given");
rot_copies(rots=rots, v=UP, cp=cp, n=n, sa=sa, delta=[r, 0, 0], subrot=subrot) children();
}
function zrot_copies(rots=[], cp=[0,0,0], n, sa=0, r, d, subrot=true, p=_NO_ARG) =
let( r = get_radius(r=r, d=d, dflt=0) )
assert(all_nonnegative([r]), "d/r must be nonnegative")
assert(subrot || r>0, "subrot can only be false if d or r is given")
rot_copies(rots=rots, v=UP, cp=cp, n=n, sa=sa, delta=[r, 0, 0], subrot=subrot, p=p);

View file

@ -1024,7 +1024,7 @@ function _normal_segment(p1,p2) =
// Usage:
// path = turtle(commands, [state], [full_state=], [repeat=])
// Description:
// Use a sequence of [turtle graphics]{https://en.wikipedia.org/wiki/Turtle_graphics} commands to generate a path. The parameter `commands` is a list of
// Use a sequence of [turtle graphics](https://en.wikipedia.org/wiki/Turtle_graphics) commands to generate a path. The parameter `commands` is a list of
// turtle commands and optional parameters for each command. The turtle state has a position, movement direction,
// movement distance, and default turn angle. If you do not give `state` as input then the turtle starts at the
// origin, pointed along the positive x axis with a movement distance of 1. By default, `turtle` returns just

View file

@ -2422,8 +2422,8 @@ module crown_gear(
// xrot(ang)
// bevel_gear(mod=3,15,35,ang,spiral=0,right_handed=true,anchor="apex")
// cyl(h=65,d=3,$fn=16,anchor=BOT);
// Example(NoAxes,VPT=[-6.28233,3.60349,15.6594],VPR=[71.1,0,52.1],VPD=213.382): Non-right angled bevel gear pair positioned in a frame, with holes cut in the frame for the shafts.
// include<BOSL2/rounding.scad>
// Example(NoAxes,VPT=[-6.28233,3.60349,15.6594],VPR=[71.1,0,52.1],VPD=213.382): Non-right angled bevel gear pair positioned in a frame, with holes cut in the frame for the shafts. Note that when rotating a gear to its appropriate angle, you must rotate around an axis tangent to the gear's pitch base, **not** the gear center. This is accomplished by shifting the gear by its pitch radius before applying the rotation.
// include <BOSL2/rounding.scad>
// angle = 60;
// t1=17; t2=29; mod=2; bot=4; wall=2; shaft=5;
// r1 = pitch_radius(mod=mod, teeth=t1);

View file

@ -592,6 +592,8 @@ module screw(spec, head, drive, thread, drive_size,
assert(is_finite(_shoulder_diam) && _shoulder_diam>=0, "Must specify nonnegative shoulder diameter")
assert(is_undef(user_thread_len) || (is_finite(user_thread_len) && user_thread_len>=0), "Must specify nonnegative thread length");
sides = max(pitch==0 ? 3 : 12, segs(nominal_diam/2));
rad_scale = _internal? (1/cos(180/sides)) : 1;
islop = _internal ? 4*get_slop() : 0;
head_height = headless || flathead ? 0
: counterbore==true || is_undef(counterbore) || counterbore==0 ? struct_val(spec, "head_height")
: counterbore;
@ -599,7 +601,8 @@ module screw(spec, head, drive, thread, drive_size,
flat_height = !flathead ? 0
: let( given_height = struct_val(spec, "head_height"))
all_positive(given_height) ? given_height
: (struct_val(spec,"head_size_sharp")+struct_val(spec,"head_oversize",0)-d_major)/2/tan(struct_val(spec,"head_angle")/2);
: (struct_val(spec,"head_size_sharp")+struct_val(spec,"head_oversize",0)-d_major*rad_scale-islop)/2/tan(struct_val(spec,"head_angle")/2);
flat_cbore_height = flathead && is_num(counterbore) ? counterbore : 0;
blunt_start1 = first_defined([blunt_start1,blunt_start,true]);
@ -650,8 +653,6 @@ module screw(spec, head, drive, thread, drive_size,
named_anchor("threads_bot", [0,0,-length-shoulder_full+offset]),
named_anchor("threads_center", [0,0,(-shank_len-length-_shoulder_len-shoulder_full-flat_height)/2+offset])
];
rad_scale = _internal? (1/cos(180/sides)) : 1;
islop = _internal ? 4*get_slop() : 0;
vnf = head=="hex" && atype=="head" && counterbore==0 ? linear_sweep(hexagon(id=head_diam*rad_scale),height=head_height,center=true) : undef;
head_diam_full = head=="hex" ? 2*head_diam/sqrt(3) : head_diam;
attach_d = in_list(atype,["threads","shank","shaft"]) ? d_major
@ -1422,8 +1423,8 @@ function _parse_drive(drive=undef, drive_size=undef) =
// ---
// details = true for more detailed model. Default: false
// counterbore = counterbore height. Default: no counterbore
// flat_height = height of flat head
// teardrop = if true make flathead and counterbores teardrop shaped with the flat 5% away from the edge of the screw. If numeric, specify the fraction of extra to add. Set to "max" for a pointed teardrop. Default: false
// flat_height = height of flat head (required for flat heads)
// teardrop = if true make flatheads and counterbores teardrop shaped with the flat 5% away from the edge of the screw. If numeric, specify the fraction of extra to add. Set to "max" for a pointed teardrop. Default: false
// slop = enlarge diameter by this extra amount (beyond that specified in the screw specification). Default: 0
function screw_head(screw_info,details=false, counterbore=0,flat_height,teardrop=false,slop=0) = no_function("screw_head");
module screw_head(screw_info,details=false, counterbore=0,flat_height,teardrop=false,slop=0) {
@ -1456,6 +1457,7 @@ module screw_head(screw_info,details=false, counterbore=0,flat_height,teardrop=f
cyl(d=d, l=counterbore, anchor=BOTTOM);
}
if (head=="flat") { // For flat head, counterbore is integrated
dummy = assert(all_positive([flat_height]), "flat_height must be given for flat heads");
angle = struct_val(screw_info, "head_angle")/2;
sharpsize = struct_val(screw_info, "head_size_sharp")+head_oversize;
sidewall_height = (sharpsize - head_size)/2 / tan(angle);

View file

@ -50,7 +50,10 @@ function struct_set(struct, key, value, grow=true) =
:
assert(is_list(key) && len(key)%2==0, "[key,value] pair list is not a list or has an odd length")
let(
new_entries = [for(i=[0:1:len(key)/2-1]) [key[2*i], key[2*i+1]]],
new_entries = [for(i=[0:1:len(key)/2-1]) if (is_def(key[2*i+1])) [key[2*i], key[2*i+1]]]
)
len(new_entries) == 0 ? struct :
let(
newkeys = column(new_entries,0),
indlist = search(newkeys, struct,0,0),
badkeys = grow ? (search([undef],new_entries,1,0)[0] != [] ? [undef] : [])

View file

@ -19,6 +19,10 @@ module test_struct_set() {
assert(st7 == [["Bar", 28],["Foo",91],[3,4],[[5,7],99]]);
st8 = struct_set(st3,[]);
assert(st8==st3);
st9 = struct_set(st2, ["Baz", undef]);
assert(st9 == st2);
st10 = struct_set(st2, ["Foo", 91, "Baz", undef]);
assert(st10 == st3);
}
test_struct_set();

211
vnf.scad
View file

@ -618,7 +618,7 @@ function _bridge(pt, outer,eps) =
// color("gray")down(.125)
// linear_extrude(height=.125)region(region);
// vnf_wireframe(vnf,width=.25);
function vnf_from_region(region, transform, reverse=false) =
function vnf_from_region(region, transform, reverse=false, triangulate=true) =
let (
region = [for (path = region) deduplicate(path, closed=true)],
regions = region_parts(force_region(region)),
@ -636,7 +636,7 @@ function vnf_from_region(region, transform, reverse=false) =
],
outvnf = vnf_join(vnfs)
)
vnf_triangulate(outvnf);
triangulate ? vnf_triangulate(outvnf) : outvnf;
@ -1618,6 +1618,213 @@ module vnf_hull(vnf, fast=false)
}
function _sort_pairs0(arr) =
len(arr)<=1 ? arr :
let(
pivot = arr[floor(len(arr)/2)][0],
lesser = [ for (y = arr) if (y[0].x < pivot.x || (y[0].x==pivot.x && y[0].y<pivot.y)) y ],
equal = [ for (y = arr) if (y[0] == pivot) y ],
greater = [ for (y = arr) if (y[0].x > pivot.x || (y[0].x==pivot.x && y[0].y>pivot.y)) y ]
)
concat( _sort_pairs0(lesser), equal, _sort_pairs0(greater) );
// Function: vnf_boundary()
// Synopsis: Returns the boundary of a VNF as an list of paths
// SynTags: VNF
// Topics: VNF Manipulation
// See Also: vnf_halfspace(), vnf_merge_points()
// Usage:
// boundary = vnf_boundary(vnf, [merge=], [idx=]);
// Description:
// Returns the boundary of a VNF as a list of paths. **The input VNF must not contain duplicate points.** By default, vnf_boundary() 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; it may produce obscure errors.
// .
// The output will be a list of closed 3D paths. If the VNF has no boundary then the output is `[]`. The boundary path(s) are
// traversed in the same direction as the edges in the original VNF.
// .
// It is sometimes desirable to have the boundary available as an index list into the VNF vertex list. However, merging the points in the VNF changes the
// VNF vertex point list. If you set `merge=false` you can also set `idx=true` to get an index list. As noted above, you must be certain
// that your in put VNF has no duplicate vertices, perhaps by running {{vnf_merge_points()}} yourself on it. With `idx=true`
// the output will be indices into the VNF vertex list, which enables you to associate the vertices on the boundary path with the original VNF.
// Arguments:
// vnf = input vnf
// ---
// merge = set to false to suppress the automatic invocation of {{vnf_merge_points()}}. Default: true
// idx = if true, return indices into VNF vertices instead of actual 3D points. Must set `merge=false` to enable this. Default: false
// Example(3D,NoAxes,VPT=[7.06325,-20.8414,20.1803],VPD=292.705,VPR=[55,0,25.7]): In this example we know that the bezier patch VNF has no duplicate vertices, so we do not need to run {{vnf_merge_points()}}.
// include <BOSL2/beziers.scad>
// patch = [
// // u=0,v=0 u=1,v=0
// [[-50,-50, 0], [-16,-50, 20], [ 16,-50, -20], [50,-50, 0]],
// [[-50,-16, 20], [-16,-16, 20], [ 16,-16, -20], [50,-16, 20]],
// [[-50, 16, 20], [-16, 16, -20], [ 16, 16, 20], [50, 16, 20]],
// [[-50, 50, 0], [-16, 50, -20], [ 16, 50, 20], [50, 50, 0]],
// // u=0,v=1 u=1,v=1
// ];
// bezvnf = bezier_vnf(patch);
// boundary = vnf_boundary(bezvnf);
// vnf_polyhedron(bezvnf);
// stroke(boundary,color="green");
// Example(3D,NoAxes,VPT=[-11.1252,-19.7333,8.39927],VPD=82.6686,VPR=[71.8,0,335.3]): An example with two path components on the boundary. The output from {{vnf_halfspace()}} can contain duplicate vertices, so we must invoke {{vnf_merge_points()}}.
// vnf = torus(id=20,od=40,$fn=28);
// cutvnf=vnf_halfspace([0,1,0,0],
// vnf_halfspace([-1,.5,-2.5,-12], vnf, closed=false),
// closed=false);
// vnf_polyhedron(cutvnf);
// 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")
let(
vnf = merge ? vnf_merge_points(vnf) : vnf,
edgelist= [ for(face=vnf[1], edge=pair(face,wrap=true))
[edge.x<edge.y ? edge : [edge.y,edge.x],edge]
],
sortedge = _sort_pairs0(edgelist),
edges= [
if (sortedge[0][0]!=sortedge[1][0]) sortedge[0][1],
for(i=[1:1:len(sortedge)-2])
if (sortedge[i][0]!=sortedge[i-1][0] && sortedge[i][0]!=sortedge[i+1][0]) sortedge[i][1],
if (last(sortedge)[0] != sortedge[len(sortedge)-2][0]) last(sortedge)[1]
],
paths = _assemble_paths(vnf[0], edges) // could be made cleaner and maybe more robust with an _assemble_path version that
) // uses edge vertex indices instead of actual point values
idx ? paths : [for(path=paths) select(vnf[0],path)];
// Function: vnf_small_offset()
// Synopsis: Computes an offset surface to a VNF for small offset distances
// SynTags: VNF
// Topics: VNF Manipulation
// See Also: vnf_sheet(), vnf_merge_points()
// Usage:
// newvnf = vnf(vnf, delta, [merge=]);
// Description:
// Computes a simple offset of a VNF by estimating the normal at every point based on the weighted average of surrounding polygons
// in the mesh. The offset distance, `delta`, must be small enough so that no self-intersection occurs, which is no issue when the
// curvature is positive (like the outside of a sphere) but for negative curvature it means the offset distance must be smaller
// than the smallest radius of curvature of the VNF. If self-intersection
// occurs, the resulting geometry will be invalid and you will get an error when you introduce a second object into the model.
// **It is your responsibility to avoid invalid geometry!** It cannot be detected automatically.
// The positive offset direction is towards the outside of the VNF, the faces that are colored yellow in the "thrown together" view.
// .
// **The input VNF must not contain duplicate points.** By default, vnf_small_offset() calls {{vnf_merge_points()}}
// to remove duplicate points. Note, however, that this operation can be slow. If you are **certain** there are no duplicate points you can
// 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.
// Arguments:
// vnf = vnf to offset
// delta = distance of offset, positive to offset out, negative to offset in
// ---
// merge = set to false to suppress the automatic invocation of {{vnf_merge_points()}}. Default: true
// Example(3D): The original sphere is on the left and an offset sphere on the right.
// vnf = sphere(d=100);
// xdistribute(spacing=125){
// vnf_polyhedron(vnf);
// vnf_polyhedron(vnf_small_offset(vnf,18));
// }
// Example(3D): The polyhedron on the left is enlarged to match the size of the offset polyhedron on the right. Note that the offset does **not** preserve coplanarity of faces. This is because the vertices all move independently, so nothing constrains faces to remain coplanar.
// include <BOSL2/polyhedra.scad>
// vnf = regular_polyhedron_info("vnf","pentagonal icositetrahedron",d=25);
// xdistribute(spacing=300){
// scale(11)vnf_polyhedron(vnf);
// vnf_polyhedron(vnf_small_offset(vnf,125));
// }
function vnf_small_offset(vnf, delta, merge=true) =
let(
vnf = merge ? vnf_merge_points(vnf) : vnf,
vertices = vnf[0],
faces = vnf[1],
vert_faces = group_data(
[for (i = idx(faces), vert = faces[i]) vert],
[for (i = idx(faces), vert = faces[i]) i]
),
normals = [for(face=faces) polygon_normal(select(vertices,face))], // Normals for each face
offset = [for(vertex=idx(vertices))
let(
vfaces = vert_faces[vertex], // Faces that surround this vertex
adjacent_normals = select(normals,vfaces),
angles = [for(faceind=vfaces)
let(
thisface = faces[faceind],
vind = search(vertex,thisface)[0]
)
vector_angle(select(vertices, select(thisface,vind-1,vind+1)))
]
)
vertices[vertex] +unit(angles*adjacent_normals)*delta
]
)
[offset,faces];
// Function: vnf_sheet()
// Synopsis: Extends a VNF into a thin sheet by extruding normal to the VNF
// SynTags: VNF
// Topics: VNF Manipulation
// See Also: vnf_small_offset(), vnf_boundary(), vnf_merge_points()
// Usage:
// newvnf = vnf_sheet(vnf, thickness, [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
// when the offset is computed, because that results in invalid geometry and will give rendering errors.
// Rendering errors may not manifest until you add other objects to your model.
// **It is your responsibility to avoid invalid geometry!**
// .
// Once the offset to the original VNF is computed the original and offset VNF are connected by filling
// in the boundary strip(s) between them
// .
// When thickness is positive, the given bezier patch is extended towards its "inside", which is the
// side that appears purple in the "thrown together" view. Note that this is the opposite direction
// of {{vnf_small_offset()}}. Extending toward the inside means that your original VNF remains unchanged
// in the output. You can extend the patch in the other direction
// 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.
// .
// **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.
// Arguments:
// vnf = vnf to process
// thickness = thickness of sheet to produce; can be positive or negative
// ---
// style = {{vnf_vertex_array()}} style to use. Default: "default"
// merge = if false then do not run {{vnf_merge_points()}}. Default: true
// Example(3D):
// 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_polyhedron(vnf_sheet(vnf,-10));
// Example(3D): This example has multiple holes
// pts = [for(x=[-10:2:10]) [ for(y=[-10:2:10]) [x,1.4*y,(-abs(x)^3+y^3)/250]]];
// vnf = vnf_vertex_array(pts);
// newface = list_remove(vnf[1], [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 = sphere(d=100, $fn=28);
// left_half()
// vnf_polyhedron(vnf_sheet(vnf,15));
function vnf_sheet(vnf, thickness, style="default", merge=true) =
let(
vnf = merge ? vnf_merge_points(vnf) : vnf,
offset = vnf_small_offset(vnf, -thickness, merge=false),
boundary = vnf_boundary(vnf,merge=false,idx=true),
newvnf = vnf_join([vnf,
vnf_reverse_faces(offset),
for(p=boundary) vnf_vertex_array([select(offset[0],p),select(vnf[0],p)],col_wrap=true,style=style)
])
)
thickness < 0 ? vnf_reverse_faces(newvnf) : newvnf;
// Section: Debugging Polyhedrons
/// Internal Module: _show_vertices()