Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vnf_sheet & bezier_sheet #1457

Merged
merged 4 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions beziers.scad
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,56 @@ function bezier_patch_normals(patch, u, v) =
: column(bezier_patch_normals(patch,u,force_list(v)),0);


// Function: bezier_sheet()
// 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:
// 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

Expand Down
4 changes: 2 additions & 2 deletions gears.scad
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
213 changes: 210 additions & 3 deletions vnf.scad
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
Expand All @@ -636,7 +636,7 @@ function vnf_from_region(region, transform, reverse=false) =
],
outvnf = vnf_join(vnfs)
)
vnf_triangulate(outvnf);
triangulate ? vnf_triangulate(outvnf) : outvnf;



Expand Down Expand Up @@ -1616,7 +1616,214 @@ module vnf_hull(vnf, fast=false)
if (is_vnf(vnf)) hull()vnf_polyhedron(vnf);
else hull_points(vnf, fast);
}




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(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(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: 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: 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-fork/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 forming a small offset
// 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:
// 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: 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: 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

Expand Down
Loading