Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Richard Milewski 2024-02-01 16:09:52 -08:00
commit d72d491ebc
15 changed files with 381 additions and 131 deletions

View file

@ -450,9 +450,6 @@ function affine3d_rot_from_to(from, to) =
];
// Function: affine3d_mirror()
// Synopsis: Returns a 3D (4x4) reflection transformation matrix.
// SynTags: Mat

View file

@ -1814,8 +1814,16 @@ module edge_profile(edges=EDGES_ALL, except=[], excess=0.01, convexity=10) {
// If no tag is set then `edge_profile_asym()` sets the tag for children to "remove" so that it will work
// with the default {{diff()}} tag. For details on specifying the edges to mask see [Specifying Edges](attachments.scad#subsection-specifying-edges).
// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments).
// Profile orientation will be made consistent for all connected edges and corners. This prohibits having three
// edges meeting at any one corner. You can intert the orientations of all edges with `flip=true`.
// .
// The asymmetric profiles are joined consistently at the corners. This is impossible if all three edges at a corner use the profile, hence
// this situation is not permitted. The profile orientation can be inverted using the `flip=true` parameter.
// .
// The standard profiles are located in the first quadrant and have positive X values. If you provide a profile located in the second quadrant,
// where the X values are negative, then it will produce a fillet. You can flip any of the standard profiles using {{xflip()}}.
// Fillets are always asymmetric because at a given edge, they can blend in two different directions, so even for symmetric profiles,
// the asymmetric logic is required. You can set the `corner_type` parameter to select rounded, chamfered or sharp corners.
// However, when the corners are inside (concave) corners, you must provide the size of the profile ([width,height]), because the
// this information is required to produce the correct corner and cannot be obtain from the profile itself, which is a child object.
// Arguments:
// edges = Edges to mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: All edges.
// except = Edges to explicitly NOT mask. See [Specifying Edges](attachments.scad#subsection-specifying-edges). Default: No edges.
@ -1890,7 +1898,7 @@ module edge_profile(edges=EDGES_ALL, except=[], excess=0.01, convexity=10) {
// cuboid(40) {
// edge_profile_asym(
// [FWD+DOWN,FWD+LEFT],
// corner_type="chamfer", size=[7,10]
// corner_type="chamfer", size=[10,10]/sqrt(2)
// ) xflip() mask2d_chamfer(10);
// }
// Example: Rounding internal corners.

View file

@ -457,7 +457,8 @@ function bezpath_points(bezpath, curveind, u, N=3) =
// [60,25], [70,0], [80,-25],
// [80,-50], [50,-50]
// ];
// debug_bezier(bez, N=3, width=2);
// path = bezpath_curve(bez);
// stroke(path,dots=true,dots_color="red");
function bezpath_curve(bezpath, splinesteps=16, N=3, endpoint=true) =
assert(is_path(bezpath))
assert(is_int(N))
@ -1251,8 +1252,8 @@ function bezier_vnf_degenerate_patch(patch, splinesteps=16, reverse=false, retur
assert(is_bezier_patch(patch), "Input is not a Bezier patch")
assert(is_int(splinesteps) && splinesteps>0, "splinesteps must be a positive integer")
let(
row_degen = [for(row=patch) all_equal(row)],
col_degen = [for(col=transpose(patch)) all_equal(col)],
row_degen = [for(row=patch) all_equal(row,eps=EPSILON)],
col_degen = [for(col=transpose(patch)) all_equal(col,eps=EPSILON)],
top_degen = row_degen[0],
bot_degen = last(row_degen),
left_degen = col_degen[0],

View file

@ -20,7 +20,7 @@
// Synopsis: Draws a line along a path or region boundry.
// SynTags: Geom
// Topics: Paths (2D), Paths (3D), Drawing Tools
// See Also: offset_stroke(), path_sweep()
// See Also: dashed_stroke(), offset_stroke(), path_sweep()
// Usage:
// stroke(path, [width], [closed], [endcaps], [endcap_width], [endcap_length], [endcap_extent], [trim]);
// stroke(path, [width], [closed], [endcap1], [endcap2], [endcap_width1], [endcap_width2], [endcap_length1], [endcap_length2], [endcap_extent1], [endcap_extent2], [trim1], [trim2]);
@ -641,8 +641,8 @@ function dashed_stroke(path, dashpat=[3,3], closed=false, fit=true, mindash=0.5)
sc = plen / tlen,
cuts = [
for (i = [0:1:reps], off = doff*sc)
let (x = i*dlen*sc + off)
if (x > 0 && x < plen) x
let (x = i*dlen*sc + off)
if (x > 0 && x < plen-EPSILON) x
],
dashes = path_cut(path, cuts, closed=false),
dcnt = len(dashes),
@ -671,7 +671,7 @@ module dashed_stroke(path, dashpat=[3,3], width=1, closed=false, fit=true, round
// Synopsis: Draws a 2D pie-slice or returns 2D or 3D path forming an arc.
// SynTags: Geom, Path
// Topics: Paths (2D), Paths (3D), Shapes (2D), Path Generators
// See Also: pie_slice(), stroke()
// See Also: pie_slice(), stroke(), ring()
//
// Usage: 2D arc from 0º to `angle` degrees.
// path=arc(n, r|d=, angle);
@ -687,10 +687,12 @@ module dashed_stroke(path, dashpat=[3,3], width=1, closed=false, fit=true, round
// path=arc(n, points=[P0,P1,P2]);
// Usage: 2D or 3D arc, fron tangent point on segment `[P0,P1]` to the tangent point on segment `[P1,P2]`.
// path=arc(n, corner=[P0,P1,P2], r=);
// Usage: Create a wedge using any other arc parameters
// path=arc(wedge=true,...)
// Usage: as module
// arc(...) [ATTACHMENTS];
// Description:
// If called as a function, returns a 2D or 3D path forming an arc.
// If called as a function, returns a 2D or 3D path forming an arc. If `wedge` is true, the centerpoint of the arc appears as the first point in the result.
// If called as a module, creates a 2D arc polygon or pie slice shape.
// Arguments:
// n = Number of vertices to form the arc curve from.
@ -706,7 +708,7 @@ module dashed_stroke(path, dashpat=[3,3], width=1, closed=false, fit=true, round
// ccw = if given with cp and 2 points takes the arc in the counter-clockwise direction. Default: false
// width = If given with `thickness`, arc starts and ends on X axis, to make a circle segment.
// thickness = If given with `width`, arc starts and ends on X axis, to make a circle segment.
// start = Start angle of arc.
// start = Start angle of arc. Default: 0
// wedge = If true, include centerpoint `cp` in output to form pie slice shape. Default: false
// endpoint = If false exclude the last point (function only). Default: true
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). (Module only) Default: `CENTER`
@ -739,16 +741,20 @@ function arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=
assert(is_bool(endpoint))
!endpoint ?
assert(!wedge, "endpoint cannot be false if wedge is true")
list_head(arc(u_add(n,1),r,angle,d,cp,points,corner,width,thickness,start,wedge,long,cw,ccw,true)) :
list_head(arc(u_add(n,1),r,angle,d,cp,points,corner,width,thickness,start,wedge,long,cw,ccw,true))
:
assert(is_undef(start) || is_def(angle), "start requires angle")
assert(is_undef(angle) || !any_defined([thickness,width,points,corner]), "Cannot give angle with points, corner, width or thickness")
assert(is_undef(n) || (is_integer(n) && n>=2), "Number of points must be an integer 2 or larger")
assert(is_undef(points) || is_path(points, [2,3]), "Points must be a list of 2d or 3d points")
assert((is_def(points) && len(points)==2) || !any([cw,ccw,long]), "cw, ccw, and long are only allowed when points is a list of length 2")
// First try for 2D arc specified by width and thickness
is_def(width) && is_def(thickness)? (
assert(!any_defined([r,cp,points]) && !any([cw,ccw,long]),"Conflicting or invalid parameters to arc")
is_def(width) && is_def(thickness)?
assert(!any_defined([r,cp,points,angle,start]),"Conflicting or invalid parameters to arc")
assert(width>0, "Width must be postive")
assert(thickness>0, "Thickness must be positive")
arc(n,points=[[width/2,0], [0,thickness], [-width/2,0]],wedge=wedge)
) :
is_def(angle)? (
: is_def(angle)?
let(
parmok = !any_defined([points,width,thickness]) &&
((is_vector(angle,2) && is_undef(start)) || is_finite(angle))
@ -769,18 +775,16 @@ function arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=
extra = wedge? [cp] : []
)
concat(extra,arcpoints)
) : is_def(corner)? (
assert(is_path(corner,[2,3]),"Point list is invalid")
: is_def(corner)?
assert(is_path(corner,[2,3]) && len(corner)==3,str("Point list is invalid"))
assert(is_undef(cp) && !any([long,cw,ccw]), "Cannot use cp, long, cw, or ccw with corner")
// Arc is 3D, so transform corner to 2D and make a recursive call, then remap back to 3D
len(corner[0]) == 3? (
assert(!(cw || ccw), "(Counter)clockwise isn't meaningful in 3d, so `cw` and `ccw` must be false")
assert(is_undef(cp) || is_vector(cp,3),"corner are 3d so cp must be 3d")
let(
plane = [is_def(cp) ? cp : corner[2], corner[0], corner[1]],
center2d = is_def(cp) ? project_plane(plane,cp) : undef,
plane = [corner[2], corner[0], corner[1]],
points2d = project_plane(plane, corner)
)
lift_plane(plane,arc(n,cp=center2d,corner=points2d,wedge=wedge,long=long))
lift_plane(plane,arc(n,corner=points2d,wedge=wedge,long=long))
) :
assert(is_path(corner) && len(corner) == 3)
let(col = is_collinear(corner[0],corner[1],corner[2]))
@ -797,12 +801,11 @@ function arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=
angle = posmod(theta_end-theta_start, 360),
arcpts = arc(n,cp=cp,r=r,start=theta_start,angle=angle,wedge=wedge)
)
dir ? arcpts : reverse(arcpts)
) :
assert(is_def(points), "Arc not specified: must give points, angle, or width and thickness")
dir ? arcpts : wedge ? reverse_polygon(arcpts) : reverse(arcpts)
: assert(is_def(points), "Arc not specified: must give points, angle, or width and thickness")
assert(is_path(points,[2,3]),"Point list is invalid")
// Arc is 3D, so transform points to 2D and make a recursive call, then remap back to 3D
len(points[0]) == 3? (
// If arc is 3D, transform points to 2D and make a recursive call, then remap back to 3D
len(points[0]) == 3?
assert(!(cw || ccw), "(Counter)clockwise isn't meaningful in 3d, so `cw` and `ccw` must be false")
assert(is_undef(cp) || is_vector(cp,3),"points are 3d so cp must be 3d")
let(
@ -811,11 +814,10 @@ function arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=
points2d = project_plane(plane, points)
)
lift_plane(plane,arc(n,cp=center2d,points=points2d,wedge=wedge,long=long))
) :
is_def(cp)? (
: len(points)==2?
// Arc defined by center plus two points, will have radius defined by center and points[0]
// and extent defined by direction of point[1] from the center
assert(is_vector(cp,2), "Centerpoint must be a 2d vector")
assert(is_vector(cp,2), "Centerpoint is required when points has length 2 and it must be a 2d vector")
assert(len(points)==2, "When pointlist has length 3 centerpoint is not allowed")
assert(points[0]!=points[1], "Arc endpoints are equal")
assert(cp!=points[0]&&cp!=points[1], "Centerpoint equals an arc endpoint")
@ -835,8 +837,7 @@ function arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=
sa = atan2(v1.y,v1.x)
)
arc(n,cp=cp,r=r,start=sa,angle=final_angle,wedge=wedge)
) : (
// Final case is arc passing through three points, starting at point[0] and ending at point[3]
: // Final case is arc passing through three points, starting at point[0] and ending at point[3]
let(col = is_collinear(points[0],points[1],points[2]))
assert(!col, "Collinear inputs do not define an arc")
let(
@ -850,8 +851,7 @@ function arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=
angle = posmod(theta_end-theta_start, 360),
arcpts = arc(n,cp=cp,r=r,start=theta_start,angle=angle,wedge=wedge)
)
dir ? arcpts : reverse(arcpts)
);
dir ? arcpts : wedge?reverse_polygon(arcpts):reverse(arcpts);
module arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge=false, anchor=CENTER, spin=0)

View file

@ -280,7 +280,7 @@ function _path_self_intersections(path, closed=true, eps=EPSILON) =
// [a1,a2]. The variable signals is zero when abs(vals[j]-ref) is less than
// eps and the sign of vals[j]-ref otherwise.
signals = [for(j=[i+2:1:plen-(i==0 && closed? 2: 1)])
abs(vals[j]-ref) < eps ? 0 : sign(vals[j]-ref) ]
abs(vals[j]-ref) < eps ? 0 : sign(vals[j]-ref) ]
)
if(max(signals)>=0 && min(signals)<=0 ) // some remaining edge intersects line [a1,a2]
for(j=[i+2:1:plen-(i==0 && closed? 3: 2)])
@ -581,6 +581,9 @@ function is_path_simple(path, closed, eps=EPSILON) =
let(closed=default(closed,false))
assert(is_path(path, 2),"Must give a 2D path")
assert(is_bool(closed))
let(
path = deduplicate(path,closed=closed,eps=eps)
)
// check for path reversals
[for(i=[0:1:len(path)-(closed?2:3)])
let(v1=path[i+1]-path[i],
@ -798,8 +801,8 @@ function path_cut(path,cutdist,closed) =
let(closed=default(closed,false))
assert(is_bool(closed))
assert(is_vector(cutdist))
assert(last(cutdist)<path_length(path,closed=closed),"Cut distances must be smaller than the path length")
assert(cutdist[0]>0, "Cut distances must be strictly positive")
assert(last(cutdist)<path_length(path,closed=closed)-EPSILON,"Cut distances must be smaller than the path length")
assert(cutdist[0]>EPSILON, "Cut distances must be strictly positive")
let(
cutlist = path_cut_points(path,cutdist,closed=closed)
)

View file

@ -2269,7 +2269,7 @@ function rounded_prism(bottom, top, joint_bot=0, joint_top=0, joint_sides=0, k_b
vnf = vnf_join([ each column(top_samples,0),
each column(bot_samples,0),
for(pts=edge_points) vnf_vertex_array(pts),
debug ? vnf_from_polygons(faces)
debug ? vnf_from_polygons(faces,fast=true)
: vnf_triangulate(vnf_from_polygons(faces))
])
)

View file

@ -552,7 +552,9 @@ module screw(spec, head, drive, thread, drive_size,
: undersize;
dummyA=assert(is_undef(undersize) || is_vector(undersize,2), "Undersize must be a scalar or 2-vector")
assert(is_undef(undersize) || num_defined([shaft_undersize, head_undersize])==0,
"Cannot combine \"undersize\" with other more specific undersize parameters");
"Cannot combine \"undersize\" with other more specific undersize parameters")
assert(is_bool(_teardrop) ||_teardrop=="max" || all_nonnegative([_teardrop]), str("Invalid teardrop parameter",_teardrop));
_teardrop = _teardrop==true ? .05 : _teardrop; // set teardrop default
shaft_undersize = first_defined([shaft_undersize, undersize[0]]);
head_undersize = first_defined([head_undersize, undersize[1]]);
dummyB=assert(is_undef(shaft_undersize) || is_finite(shaft_undersize), "shaft_undersize must be a number")
@ -683,8 +685,9 @@ module screw(spec, head, drive, thread, drive_size,
slop=islop,teardrop=_teardrop);
if (_shoulder_len>0)
up(eps_shoulder-flat_height){
if (_teardrop)
teardrop(d=_shoulder_diam*rad_scale+islop, h=_shoulder_len+eps_shoulder, anchor=FRONT, orient=BACK, $fn=sides);
if (_teardrop!=false) //////
teardrop(d=_shoulder_diam*rad_scale+islop,cap_h=is_num(_teardrop) ? (_shoulder_diam*rad_scale+islop)/2*(1+_teardrop):undef,
h=_shoulder_len+eps_shoulder, anchor=FRONT, orient=BACK, $fn=sides);
else
cyl(d=_shoulder_diam*rad_scale+islop, h=_shoulder_len+eps_shoulder, anchor=TOP, $fn=sides, chamfer1=details ? _shoulder_diam/30:0);
}
@ -702,8 +705,9 @@ module screw(spec, head, drive, thread, drive_size,
: bevel2=="reverse" ? -bevsize
: bevel2;
down(_shoulder_len+flat_height-eps_shank)
if (_teardrop)
teardrop(d=d_major*rad_scale+islop, h=L+eps_shank, anchor=FRONT, orient=BACK, $fn=sides, chamfer1=bev1, chamfer2=bev2);
if (_teardrop!=false) ///////
teardrop(d=d_major*rad_scale+islop, cap_h=is_num(_teardrop) ? (d_major*rad_scale+islop)/2*(1+_teardrop) : undef,
h=L+eps_shank, anchor=FRONT, orient=BACK, $fn=sides, chamfer1=bev1, chamfer2=bev2);
else
cyl(d=d_major*rad_scale+islop, h=L+eps_shank, anchor=TOP, $fn=sides, chamfer1=bev1, chamfer2=bev2);
}
@ -773,7 +777,7 @@ module screw(spec, head, drive, thread, drive_size,
// head = head type. See [screw heads](#subsection-screw-heads) Default: none
// ---
// thread = thread type or specification for threaded masks, true to make a threaded mask with the standard threads, or false to make an unthreaded mask. See [screw pitch](#subsection-standard-screw-pitch). Default: false
// teardrop = if true produce teardrop hole. Default: false
// teardrop = If true, adds a teardrop profile to the hole for 3d printability of horizontal holes. If numeric, specifies the proportional extra distance of the teardrop flat top from the screw center, or set to "max" for a pointed teardrop. Default: false
// oversize = amount to increase diameter of the screw hole (hole and countersink). A scalar or length 2 vector. Default: use computed tolerance
// hole_oversize = amount to increase diameter of the hole. Overrides the use of tolerance and replaces any settings given in the screw specification.
// head_oversize = amount to increase diameter of head. Overrides the user of tolerance and replaces any settings given in the screw specification.
@ -1419,7 +1423,7 @@ 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
// 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
// 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) {
@ -1428,7 +1432,9 @@ module screw_head(screw_info,details=false, counterbore=0,flat_height,teardrop=f
head = struct_val(screw_info, "head");
head_size = struct_val(screw_info, "head_size",0) + head_oversize;
head_height = struct_val(screw_info, "head_height");
dum0=assert(is_def(head_height) || in_list(head,["flat","none"]), "Undefined head height only allowed with flat head or headless screws");
dum0=assert(is_def(head_height) || in_list(head,["flat","none"]), "Undefined head height only allowed with flat head or headless screws")
assert(is_bool(teardrop) || teardrop=="max" || all_nonnegative([teardrop]),"Teardrop parameter invalid");
teardrop = teardrop==true ? .05 : teardrop;
heightok = (is_undef(head_height) && in_list(head,["flat","none"])) || all_positive(head_height);
dum1=assert(heightok, "Head hight must be a postive number");
dum2=assert(counterbore==0 || counterbore==false || head!="none", "Cannot counterbore a headless screw");
@ -1444,8 +1450,8 @@ module screw_head(screw_info,details=false, counterbore=0,flat_height,teardrop=f
union(){
if (head!="flat" && counterbore>0){
d = head=="hex"? 2*head_size/sqrt(3) : head_size;
if (teardrop)
teardrop(d=d, l=counterbore, orient=BACK, anchor=BACK);
if (teardrop!=false)
teardrop(d=d, l=counterbore, cap_h=is_num(teardrop) ? d/2*(1+teardrop):undef, orient=BACK, anchor=BACK);
else
cyl(d=d, l=counterbore, anchor=BOTTOM);
}
@ -1458,8 +1464,8 @@ module screw_head(screw_info,details=false, counterbore=0,flat_height,teardrop=f
r1 = head_size/2;
r2 = r1 - tan(angle)*slopeheight;
n = segs(r1);
prof1 = teardrop ? teardrop2d(r=r1,$fn=n) : circle(r=r1, $fn=n);
prof2 = teardrop ? teardrop2d(r=r2,$fn=n) : circle(r=r2, $fn=n);
prof1 = teardrop!=false ? teardrop2d(r=r1,cap_h=is_num(teardrop)?r1*(1+teardrop):undef,$fn=n) : circle(r=r1, $fn=n);
prof2 = teardrop!=false ? teardrop2d(r=r2,cap_h=is_num(teardrop)?r2*(1+teardrop):undef,$fn=n) : circle(r=r2, $fn=n);
skin([prof2,prof1,prof1], z=[-flat_height, -flat_height+slopeheight, counterbore],slices=0);
}
if (head!="flat" && counterbore==0) {

View file

@ -214,7 +214,7 @@ function rect(size=1, rounding=0, chamfer=0, atype="box", anchor=CENTER, spin=0,
assert(is_undef(cornerpt) || len(cornerpt)==1,"Cannot find corner point to anchor")
[move(cp, p=qrpts), is_undef(cornerpt)? undef : move(cp,p=cornerpt[0])]
],
path = flatten(column(corners,0)),
path = deduplicate(flatten(column(corners,0)),closed=true),
override = [for(i=[0:3])
let(quad=quadorder[i])
if (is_def(corners[i][1])) [quadpos[quad], [corners[i][1], min(chamfer[quad],rounding[quad])<0 ? [quadpos[quad].x,0] : undef]]]
@ -1503,6 +1503,179 @@ module egg(length,r1,r2,R,d1,d2,D,anchor=CENTER, spin=0)
}
// Function&Module: ring()
// Synopsis: Draws a 2D ring or partial ring or returns a region or path
// SynTags: Geom, Region, Path
// Topics: Shapes (2D), Paths (2D), Path Generators, Regions, Attachable
// See Also: arc(), circle()
//
// Usage: ring or partial ring from radii/diameters
// region=ring(n, r1=|d1=, r2=|d2=, [full=], [angle=], [start=]);
// Usage: ring or partial ring from radius and ring width
// region=ring(n, ring_width, r=|d=, [full=], [angle=], [start=]);
// Usage: ring or partial ring passing through three points
// region=ring(n, [ring_width], [r=,d=], points=[P0,P1,P2], [full=]);
// Usage: ring or partial ring from tangent point on segment `[P0,P1]` to the tangent point on segment `[P1,P2]`.
// region=ring(n, [ring_width], corner=[P0,P1,P2], [r=,d=], [r1|d1=], [r2=|d2=], [full=]);
// Usage: ring or partial ring based on setting a width at the X axis and height above the X axis
// region=ring(n, [ring_width], [r=|d=], width=, thickness=, [full=]);
// Usage: as a module
// ring(...) [ATTACHMENTS];
// Description:
// If called as a function returns a region or path for a ring or part of a ring. If called as a module, creates the corresponding 2D ring or partial ring shape.
// The geometry of the ring can be specified using any of the methods supported by {{arc()}}. If `full` is true (the default) the ring will be complete and the
// returned value a region. If `full` is false then the return is a path describing a partial ring. The returned path is always clockwise with the larger radius arc first.
// A ring has two radii, the inner and outer. When specifying geometry you must somehow specify one radius, which can be directly with `r=` or `r1=` or by giving a point list with
// or without a center point. You specify the second radius by giving `r=` directly, or `r2=` if you used `r1=` for the first radius, or by giving `ring_width`. If `ring_width`
// the second radius will be larger than the first; if `ring_width` is negative the second radius will be smaller.
// Arguments:
// n = Number of vertices to use for the inner and outer portions of the ring
// ring_width = width of the ring. Can be positive or negative
// ---
// r1/d1 = inner radius or diameter of the ring
// r2/d2 = outer radius or diameter of the ring
// r/d = second radius or diameter of ring when r1 or d1 are not given
// full = if true create a full ring, if false create a partial ring. Default: true unless `angle` is given
// cp = Centerpoint of ring.
// points = Points on the ring boundary.
// corner = A path of two segments to fit the ring tangent to.
// long = if given with cp and points takes the long arc instead of the default short arc. Default: false
// cw = if given with cp and 2 points takes the arc in the clockwise direction. Default: false
// ccw = if given with cp and 2 points takes the arc in the counter-clockwise direction. Default: false
// width = If given with `thickness`, ring is defined based on an arc with ends on X axis.
// thickness = If given with `width`, ring is defined based on an arc with ends on X axis, and this height above the X axis.
// start = Start angle of ring. Default: 0
// angle = If scalar, the end angle in degrees relative to start parameter. If a vector specifies start and end angles of ring.
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). (Module only) Default: `CENTER`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). (Module only) Default: `0`
// Examples(2D):
// ring(r1=5,r2=7, n=32);
// ring(r=5,ring_width=-1, n=32);
// ring(r=7, n=5, ring_width=-4);
// ring(points=[[0,0],[3,3],[5,2]], ring_width=2, n=32);
// ring(points=[[0,0],[3,3],[5,2]], r=1, n=32);
// ring(cp=[3,3], points=[[4,4],[1,3]], ring_width=1);
// ring(corner=[[0,0],[4,4],[7,3]], r2=2, r1=1.5,n=22,full=false);
// ring(r1=5,r2=7, angle=[33,110], n=32);
// ring(r1=5,r2=7, angle=[0,360], n=32); // full circle
// ring(r=5, points=[[0,0],[3,3],[5,2]], full=false, n=32);
// ring(32,-2, cp=[1,1], points=[[4,4],[-3,6]], full=false);
// ring(r=5,ring_width=-1, n=32);
// ring(points=[[0,0],[3,3],[5,2]], ring_width=2, n=32);
// ring(points=[[0,0],[3,3],[5,2]], r=1, n=32);
// ring(cp=[3,3], points=[[4,4],[1,3]], ring_width=1);
// Example(2D): Using corner, the outer radius is the one tangent to the corner
// corner = [[0,0],[4,4],[7,3]];
// ring(corner=corner, r2=3, r1=2,n=22);
// stroke(corner, width=.1,color="red");
// Example(2D): For inner radius tangent to a corner, specify `r=` and `ring_width`.
// corner = [[0,0],[4,4],[7,3]];
// ring(corner=corner, r=3, ring_width=1,n=22,full=false);
// stroke(corner, width=.1,color="red");
// Example(2D):
// $fn=128;
// region = ring(width=5,thickness=1.5,ring_width=2);
// path = ring(width=5,thickness=1.5,ring_width=2,full=false);
// stroke(region,width=.25);
// color("red") dashed_stroke(path,dashpat=[1.5,1.5],closed=true,width=.25);
module ring(n,ring_width,r,r1,r2,angle,d,d1,d2,cp,points,corner, width,thickness,start, long=false, full=true, cw=false,ccw=false, anchor=CENTER, spin=0)
{
R = ring(n=n,r=r,ring_width=ring_width,r1=r1,r2=r2,angle=angle,d=d,d1=d1,d2=d2,cp=cp,points=points,corner=corner, width=width,thickness=thickness,start=start,
long=long, full=full, cw=cw, ccw=ccw);
attachable(anchor,spin,two_d=true,region=is_region(R)?R:undef,path=is_region(R)?undef:R,extent=false) {
region(R);
children();
}
}
function ring(n,ring_width,r,r1,r2,angle,d,d1,d2,cp,points,corner, width,thickness,start, long=false, full=true, cw=false,ccw=false) =
let(
r1 = is_def(r1) ? assert(is_undef(d),"Cannot define r1 and d1")r1
: is_def(d1) ? d1/2
: undef,
r2 = is_def(r2) ? assert(is_undef(d),"Cannot define r2 and d2")r2
: is_def(d2) ? d2/2
: undef,
r = is_def(r) ? assert(is_undef(d),"Cannot define r and d")r
: is_def(d) ? d/2
: undef,
full = is_def(angle) ? false : full
)
assert(is_undef(start) || is_def(angle), "start requires angle")
assert(is_undef(angle) || !any_defined([thickness,width,points,corner]), "Cannot give angle with points, corner, width or thickness")
assert(!is_vector(angle,2) || abs(angle[1]-angle[0]) <= 360, "angle gives more than 360 degrees")
assert(is_undef(points) || is_path(points,2), str("Points must be a 2d vector",points))
assert(!any_defined([points,thickness,width]) || num_defined([r1,r2])==0, "Cannot give r1, r2, d1, or d2 with points, width or thickness")
is_def(width) && is_def(thickness)?
assert(!any_defined([r,cp,points,angle,start]), "Conflicting or invalid parameters to ring")
assert(all_positive([width,thickness]), "Width and thickness must be positive")
ring(n=n,r=r,ring_width=ring_width,points=[[width/2,0], [0,thickness], [-width/2,0]],full=full)
: full && is_undef(cp) && is_def(points) ?
assert(is_def(points) && len(points)==3, "Without cp given, must provide exactly three points")
assert(num_defined([r,ring_width]), "Must give r or ring_width with point list")
let(
ctr_rad = circle_3points(points),
dummy=assert(is_def(ctr_rad[0]), "Collinear points given to ring()"),
part1 = move(ctr_rad[0],circle(r=ctr_rad[1], $fn=is_def(n) ? n : $fn)),
first_r = norm(part1[0]-ctr_rad[0]),
r = is_def(r) ? r : first_r+ring_width,
part2 = move(ctr_rad[0],circle(r=r, $fn=is_def(n) ? n : $fn))
)
assert(first_r!=r, "Ring has zero width")
(first_r>r ? [part1, reverse(part2)] : [part2, reverse(part1)])
: full && is_def(corner) ?
assert(is_path(corner,2) && len(corner)==3, "corner must be a list of 3 points")
assert(!any_defined([thickness,width,points,cp,angle.start]), "Conflicting or invalid parameters to ring")
let(parmok = (all_positive([r1,r2]) && num_defined([r,ring_width])==0)
|| (num_defined([r1,r2])==0 && all_positive([r]) && is_finite(ring_width)))
assert(parmok, "With corner must give (r1 and r2) or (r and ring_width), but you gave some other combination")
let(
newr1 = is_def(r1) ? min(r1,r2) : min(r,r+ring_width),
newr2 = is_def(r2) ? max(r2,r1) : max(r,r+ring_width),
data = circle_2tangents(newr2,corner[0],corner[1],corner[2]),
cp=data[0]
)
[move(cp,circle($fn=is_def(n) ? n : $fn, r=newr2)),move(cp, circle( $fn=is_def(n) ? n : $fn, r=newr1))]
: full && is_def(cp) && is_def(points) ?
assert(in_list(len(points),[1,2]), "With cp must give a list of one or two points.")
assert(num_defined([r,ring_width]), "Must give r or ring_width with point list")
let(
first_r=norm(points[0]-cp),
part1 = move(cp,circle(r=first_r, $fn=is_def(n) ? n : $fn)),
r = is_def(r) ? r : first_r+ring_width,
part2 = move(cp,circle(r=r, $fn=is_def(n) ? n : $fn))
)
assert(first_r!=r, "Ring has zero width")
first_r>r ? [part1, reverse(part2)] : [part2, reverse(part1)]
: full || angle==360 || (is_vector(angle,2) && abs(angle[1]-angle[0])==360) ?
let(parmok = (all_positive([r1,r2]) && num_defined([r,ring_width])==0)
|| (num_defined([r1,r2])==0 && all_positive([r]) && is_finite(ring_width)))
assert(parmok, "Must give (r1 and r2) or (r and ring_width), but you gave some other combination")
let(
newr1 = is_def(r1) ? min(r1,r2) : min(r,r+ring_width),
newr2 = is_def(r2) ? max(r2,r1) : max(r,r+ring_width),
cp = default(cp,[0,0])
)
[move(cp,circle($fn=is_def(n) ? n : $fn, r=newr2)),move(cp, circle( $fn=is_def(n) ? n : $fn, r=newr1))]
: let(
parmRok = (all_positive([r1,r2]) && num_defined([r,ring_width])==0)
|| (num_defined([r1,r2])==0 && all_positive([r]) && is_finite(ring_width)),
pass_r = any_defined([points,thickness]) ? assert(!any_defined([r1,r2]),"Cannot give r1, d1, r2, or d2 with a point list or width & thickness")
assert(num_defined([ring_width,r])==1, "Must defined exactly one of r and ring_width when using a pointlist or width & thickness")
undef
: assert(num_defined([r,r2])==1,"Cannot give r or d and r1 or d1") first_defined([r,r2]),
base_arc = clockwise_polygon(arc(r=pass_r,n=n,angle=angle,cp=cp,points=points, corner=corner, width=width, thickness=thickness,start=start, long=long, cw=cw,ccw=ccw,wedge=true)),
center = base_arc[0],
arc1 = list_tail(base_arc,1),
r_actual = norm(center-arc1[0]),
new_r = is_def(ring_width) ? r_actual+ring_width
: first_defined([r,r1]),
pts = [center+new_r*unit(arc1[0]-center), center+new_r*unit(arc1[floor(len(arc1)/2)]-center), center+new_r*unit(last(arc1)-center)],
second=arc(n=n,points=pts),
arc2 = is_polygon_clockwise(second) ? second : reverse(second)
) new_r>r_actual ? concat(arc2, reverse(arc1)) : concat(arc1,reverse(arc2));
// Function&Module: glued_circles()
// Synopsis: Creates a shape of two circles joined by a curved waist.

View file

@ -1702,7 +1702,7 @@ module cyl(
cylinder(h=l, r1=r1, r2=r2, center=true, $fn=sides);
} else {
vnf = cyl(
l=l, r1=r1, r2=r2, center=true, $fn=sides,
l=l, r1=r1, r2=r2, center=true,
chamfer=chamfer, chamfer1=chamfer1, chamfer2=chamfer2,
chamfang=chamfang, chamfang1=chamfang1, chamfang2=chamfang2,
rounding=rounding, rounding1=rounding1, rounding2=rounding2,

View file

@ -1832,13 +1832,14 @@ function path_sweep(shape, path, method="incremental", normal, closed, twist=0,
spathfrac = scale_by_length ? path_length_fractions(path, closed) : [for(i=[0:1:len(path)]) i / (len(path)-(closed?0:1))],
L = len(path),
unscaled_transform_list =
method=="incremental" ?
method=="old_incremental" ?
let(rotations =
[for( i = 0,
ynormal = normal - (normal * tangents[0])*tangents[0],
rotation = frame_map(y=ynormal, z=tangents[0])
;
i < len(tangents) + (closed?1:0) ;
i < len(tangents) + (closed?1:0)
;
rotation = i<len(tangents)-1+(closed?1:0)? rot(from=tangents[i],to=tangents[(i+1)%L])*rotation : undef,
i=i+1
)
@ -1864,6 +1865,49 @@ function path_sweep(shape, path, method="incremental", normal, closed, twist=0,
adjusted_final = !closed ? undef :
translate(path[0]) * rotations[0] * zrot(-correction_twist+correction_twist%(360/symmetry)-twist)
) [for(i=idx(path)) translate(path[i]) * rotations[i] * zrot((twistfix-twist)*tpathfrac[i]), if(closed) adjusted_final]
: method=="incremental" ? // Implements Rotation Minimizing Frame from "Computation of Rotation Minimizing Frames"
// by Wenping Yang, Bert Büttler, Dayue Zheng, Yang Liu, 2008
// http://doi.acm.org/10.1145/1330511.1330513
let(rotations = // https://www.microsoft.com/en-us/research/wp-content/uploads/2016/12/Computation-of-rotation-minimizing-frames.pdf
[for( i = 0,
ynormal = normal - (normal * tangents[0])*tangents[0],
rotation = frame_map(y=ynormal, z=tangents[0]),
r=ynormal
;
i < len(tangents) + (closed?1:0)
;
v1 = path[(i+1)%L]-path[i%L],
c1 = v1*v1,
rL = r - 2*(v1*r)/c1 * v1,
tL = tangents[i%L] - 2*(v1*tangents[i%L])/c1 * v1,
v2 = tangents[(i+1)%L]-tL,
c2 = v2*v2,
r = rL - (2/c2)*(v2*rL)*v2,
rotation = i<len(tangents)-1+(closed?1:0)? frame_map(y=r,z=tangents[(i+1)%L]) : undef,
i=i+1
)
rotation],
// The mismatch is the inverse of the last transform times the first one for the closed case, or the inverse of the
// desired final transform times the realized final transform in the open case. Note that when closed==true the last transform
// is a actually looped around and applies to the first point position, so if we got back exactly where we started
// then it will be the identity, but we might have accumulated some twist which will show up as a rotation around the
// X axis. Similarly, in the closed==false case the desired and actual transformations can only differ in the twist,
// so we can need to calculate the twist angle so we can apply a correction, which we distribute uniformly over the whole path.
reference_rot = closed ? rotations[0] :
is_undef(last_normal) ? last(rotations) :
let(
last_tangent = last(tangents),
lastynormal = last_normal - (last_normal * last_tangent) * last_tangent
)
frame_map(y=lastynormal, z=last_tangent),
mismatch = transpose(last(rotations)) * reference_rot,
correction_twist = atan2(mismatch[1][0], mismatch[0][0]),
// Spread out this extra twist over the whole sweep so that it doesn't occur
// abruptly as an artifact at the last step.
twistfix = correction_twist%(360/symmetry),
adjusted_final = !closed ? undef :
translate(path[0]) * rotations[0] * zrot(-correction_twist+correction_twist%(360/symmetry)-twist)
) [for(i=idx(path)) translate(path[i]) * rotations[i] * zrot((twistfix-twist)*tpathfrac[i]), if(closed) adjusted_final]
: method=="manual" ?
[for(i=[0:L-(closed?0:1)]) let(
ynormal = relaxed ? normals[i%L] : normals[i%L] - (normals[i%L] * tangents[i%L])*tangents[i%L],
@ -1873,21 +1917,6 @@ function path_sweep(shape, path, method="incremental", normal, closed, twist=0,
assert(approx(ynormal*znormal,0),str("Supplied normal is parallel to the path tangent at point ",i))
translate(path[i%L])*rotation*zrot(-twist*tpathfrac[i])
]
: method=="cross"?
let(
crossnormal_mid = [for(i=[(closed?0:1):L-(closed?1:2)])
let(v= cross( select(path,i+1)-path[i], path[i]-select(path,i-1)),
f=assert(norm(v)>EPSILON)
)
v
],
crossnormal = closed ? crossnormal_mid : [crossnormal_mid[0], each crossnormal_mid, last(crossnormal_mid)]
)
[for(i=[0:L-(closed?0:1)]) let(
rotation = frame_map(x=crossnormal[i%L], z=tangents[i%L])
)
translate(path[i%L])*rotation*zrot(-twist*tpathfrac[i])
]
: method=="natural" ? // map x axis of shape to the path normal, which points in direction of curvature
let (pathnormal = path_normals(path, tangents, closed))
assert(all_defined(pathnormal),"Natural normal vanishes on your curve, select a different method")
@ -2940,7 +2969,7 @@ function associate_vertices(polygons, split, curpoly=0) =
// rect(30), texture=tex, h=30,
// tex_size=[10,10]
// );
// Example(3D): **"cones"** (VNF) = Raised conical spikes. Specify `$fn` to set the number of segments on the cone (will be rounded to a multiple of 4). If you use $fa and $fs then the number of segments is determined for the original VNF scale of 1x1. Giving `border=` specifies the horizontal border width between the edge of the tile and the base of the cone. The `border` value must be nonnegative and smaller than 0.5. Default: 0.
// Example(3D): **"cones"** (VNF) = Raised conical spikes. Specify `$fn` to set the number of segments on the cone (will be rounded to a multiple of 4). The default is `$fn=16`. Note that `$fa` and `$fs` are ignored, since the scale of the texture is unknown at the time of definition. Giving `border=` specifies the horizontal border width between the edge of the tile and the base of the cone. The `border` value must be nonnegative and smaller than 0.5. Default: 0.
// tex = texture("cones", $fn=16);
// linear_sweep(
// rect(30), texture=tex, h=30, tex_depth=3,
@ -2982,13 +3011,13 @@ function associate_vertices(polygons, split, curpoly=0) =
// rect(30), texture=tex, h=30,
// tex_size=[10,10]
// );
// Example(3D): **"dimples"** (VNF) = Round divots. Specify `$fn` to set the number of segments on the cone (will be rounded to a multiple of 4). If you use $fa and $fs then the number of segments is determined for the original VNF scale of 1x1. Giving `border=` specifies the horizontal width of the flat border region between the tile edges and the edge of the dimple. Must be nonnegative and strictly less than 0.5. Default: 0.05.
// Example(3D): **"dimples"** (VNF) = Round divots. Specify `$fn` to set the number of segments on the dimples (will be rounded to a multiple of 4). The default is `$fn=16`. Note that `$fa` and `$fs` are ignored, since the scale of the texture is unknown at the time of definition. Giving `border=` specifies the horizontal width of the flat border region between the tile edges and the edge of the dimple. Must be nonnegative and strictly less than 0.5. Default: 0.05.
// tex = texture("dimples", $fn=16);
// linear_sweep(
// rect(30), texture=tex, h=30,
// tex_size=[10,10]
// );
// Example(3D): **"dots"** (VNF) = Raised round bumps. Specify `$fn` to set the number of segments on the cone (will be rounded to a multiple of 4). If you use $fa and $fs then the number of segments is determined for the original VNF scale of 1x1. Giving `border=` specifies the horizontal width of the flat border region between the tile edge and the edge of the dots. Must be nonnegative and strictly less than 0.5. Default: 0.05.
// Example(3D): **"dots"** (VNF) = Raised round bumps. Specify `$fn` to set the number of segments on the dots (will be rounded to a multiple of 4). The default is `$fn=16`. Note that `$fa` and `$fs` are ignored, since the scale of the texture is unknown at the time of definition. Giving `border=` specifies the horizontal width of the flat border region between the tile edge and the edge of the dots. Must be nonnegative and strictly less than 0.5. Default: 0.05.
// tex = texture("dots", $fn=16);
// linear_sweep(
// rect(30), texture=tex, h=30,
@ -3129,6 +3158,8 @@ function associate_vertices(polygons, split, curpoly=0) =
// );
function _tex_fn_default() = 16;
__vnf_no_n_mesg=" texture is a VNF so it does not accept n. Set sample rate for VNF textures using the tex_samples parameter to cyl(), linear_sweep() or rotate_sweep().";
function texture(tex, n, border, gap, roughness, inset) =
@ -3343,7 +3374,7 @@ function texture(tex, n, border, gap, roughness, inset) =
assert(num_defined([gap,roughness])==0, "cones texture does not accept gap or roughness")
let(
border = default(border,0),
n = quant(segs(1/2-border),4)
n = $fn > 0 ? quantup($fn,4) : _tex_fn_default()
)
assert(border>=0 && border<0.5)
[
@ -3398,7 +3429,7 @@ function texture(tex, n, border, gap, roughness, inset) =
assert(num_defined([gap,roughness])==0, str(tex," texture does not accept gap or roughness"))
let(
border = default(border,0.05),
n = quant(segs(1/2-border),4)
n = $fn > 0 ? quantup($fn,4) : _tex_fn_default()
)
assert(border>=0 && border < 0.5)
let(
@ -3622,7 +3653,7 @@ function _textured_linear_sweep(
let(
caps = is_bool(caps) ? [caps,caps] : caps,
regions = is_path(region,2)? [[region]] : region_parts(region),
tex = is_string(texture)? texture(texture) : texture,
tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture,
dummy = assert(is_undef(samples) || is_vnf(tex), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"),
dummy2=is_bool(rot)?echo("boolean value for tex_rot is deprecated. Use a numerical angle, one of 0, 90, 180, or 270.")0:0,
texture = !rot? tex :
@ -3870,7 +3901,7 @@ function _textured_revolution(
counts, samples,
style="min_edge", atype="intersect",
anchor=CENTER, spin=0, orient=UP
) =
) =
assert(angle>0 && angle<=360)
assert(is_path(shape,[2]) || is_region(shape))
assert(is_undef(samples) || is_int(samples))
@ -3897,7 +3928,7 @@ function _textured_revolution(
)
assert(closed || is_path(shape,2))
let(
tex = is_string(texture)? texture(texture) : texture,
tex = is_string(texture)? texture(texture,$fn=_tex_fn_default()) : texture,
dummy = assert(is_undef(samples) || is_vnf(tex), "You gave the tex_samples argument with a heightfield texture, which is not permitted. Use the n= argument to texture() instead"),
dummy2=is_bool(rot)?echo("boolean value for tex_rot is deprecated. Use a numerical angle, one of 0, 90, 180, or 270.")0:0,
texture = !rot? tex :

View file

@ -298,5 +298,11 @@ test_path_torsion();
//echo(fmt_float(sampled));
module test_is_path_simple(){
assert(is_path_simple([[0,0],[1,1],[1,1],[2,1]]));
assert(is_path_simple([[0,0],[10,0],[0,20],[10,20]],closed=false));
assert(!is_path_simple([[0,0],[10,0],[0,20],[10,20]],closed=true));
assert(is_path_simple(circle($fn=20, r=10)));
}

View file

@ -36,9 +36,9 @@ test_vnf_faces();
module test_vnf_from_polygons() {
verts = [[-1,-1,-1],[1,-1,-1],[0,1,-1],[0,0,1]];
faces = [[0,1,2],[0,1,3,2],[2,3,0]];
faces = [[0,1,2],[0,3,1],[2,3,0],[0,1,0]]; // Last face has zero area
assert(vnf_merge_points(
vnf_from_polygons([for (face=faces) select(verts,face)])) == [verts,faces]);
vnf_from_polygons([for (face=faces) select(verts,face)])) == [verts,select(faces,0,-2)]);
}
test_vnf_from_polygons();

View file

@ -170,7 +170,7 @@
// lead_in_ang1 = Specify angular length in degrees of the lead in section of the threading at the bottom with blunt start threads
// lead_in_ang2 = Specify angular length in degrees of the lead in section of the threading at the top with blunt start threads
// lead_in_shape = Specify the shape of the thread lead in by giving a text string or function. Default: "default"
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, to help with making a threaded hole mask. Default: false
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, for 3d printability of horizontal holes. If numeric, specifies the proportional extra distance of the teardrop flat top from the screw center, or set to "max" for a pointed teardrop. Default: false
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
@ -496,7 +496,7 @@ module threaded_nut(
// lead_in_ang1 = Specify angular length in degrees of the lead in section of the threading at the bottom with blunt start threads
// lead_in_ang2 = Specify angular length in degrees of the lead in section of the threading at the top with blunt start threads
// lead_in_shape = Specify the shape of the thread lead in by giving a text string or function. Default: "default"
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, to help with making a threaded hole mask. Default: false
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, for 3d printability of horizontal holes. If numeric, specifies the proportional extra distance of the teardrop flat top from the screw center, or set to "max" for a pointed teardrop. Default: false
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
@ -765,7 +765,7 @@ module trapezoidal_threaded_nut(
// lead_in_ang1 = Specify angular length in degrees of the lead in section of the threading at the bottom with blunt start threads
// lead_in_ang2 = Specify angular length in degrees of the lead in section of the threading at the top with blunt start threads
// lead_in_shape = Specify the shape of the thread lead in by giving a text string or function. Default: "default"
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, to help with making a threaded hole mask. Default: false
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, for 3d printability of horizontal holes. If numeric, specifies the proportional extra distance of the teardrop flat top from the screw center, or set to "max" for a pointed teardrop. Default: false
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
@ -1102,7 +1102,7 @@ module npt_threaded_rod(
// lead_in_ang1 = Specify angular length in degrees of the lead in section of the threading at the bottom with blunt start threads
// lead_in_ang2 = Specify angular length in degrees of the lead in section of the threading at the top with blunt start threads
// lead_in_shape = Specify the shape of the thread lead in by giving a text string or function. Default: "default"
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, to help with making a threaded hole mask. Default: false
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, for 3d printability of horizontal holes. If numeric, specifies the proportional extra distance of the teardrop flat top from the screw center, or set to "max" for a pointed teardrop. Default: false
// d1 = Bottom outside diameter of threads.
// d2 = Top outside diameter of threads.
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
@ -1318,7 +1318,7 @@ module buttress_threaded_nut(
// lead_in_ang1 = Specify angular length in degrees of the lead in section of the threading at the bottom with blunt start threads
// lead_in_ang2 = Specify angular length in degrees of the lead in section of the threading at the top with blunt start threads
// lead_in_shape = Specify the shape of the thread lead in by giving a text string or function. Default: "default"
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, to help with making a threaded hole mask. Default: false
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, for 3d printability of horizontal holes. If numeric, specifies the proportional extra distance of the teardrop flat top from the screw center, or set to "max" for a pointed teardrop. Default: false
// d1 = Bottom outside diameter of threads.
// d2 = Top outside diameter of threads.
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
@ -1625,6 +1625,11 @@ module ball_screw_rod(
// running off the end of the shaft and leaving a sharp edged partial thread at the end of the screw. This makes
// screws easier to start and prevents cross threading. Blunt start threads should always be superior, and they are
// faster to model, but if you really need standard threads that run off the end you can set `blunt_start=false`.
// .
// The teardrop option cuts off the threads with a teardrop for 3d printability of horizontal holes. By default,
// if the screw outer radius is r then the flat top will be at distance 1.05r from the center, adding a 5% space.
// You can set teardrop to a numerical value to adjust that percentage, e.g. a value of 0.1 would give a 10% space.
// You can set teardrop to "max" to create a pointy-top teardrop with no flat section.
// Arguments:
// d = Outer diameter of threaded rod.
// l / length / h / height = Length of threaded rod.
@ -1652,7 +1657,7 @@ module ball_screw_rod(
// lead_in_ang1 = Specify angular length in degrees of the lead in section of the threading at the bottom with blunt start threads
// lead_in_ang2 = Specify angular length in degrees of the lead in section of the threading at the top with blunt start threads
// lead_in_shape = Specify the shape of the thread lead in by giving a text string or function. Default: "default"
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, to help with making a threaded hole mask. Default: false
// teardrop = If true, adds a teardrop profile to the back (Y+) side of the threaded rod, for 3d printability of horizontal holes. If numeric, specifies the proportional extra distance of the teardrop flat top from the screw center, or set to "max" for a pointed teardrop (see above). Default: false
// anchor = Translate so anchor point is at origin (0,0,0). See [anchor](attachments.scad#subsection-anchor). Default: `CENTER`
// spin = Rotate this many degrees around the Z axis after anchor. See [spin](attachments.scad#subsection-spin). Default: `0`
// orient = Vector to rotate top towards, after spin. See [orient](attachments.scad#subsection-orient). Default: `UP`
@ -1922,9 +1927,30 @@ module generic_threaded_rod(
up(len/2+.001)cyl(l=-clip_bev2, r1=r2adj+profmin, r2=r2adj+profmin+slope*clip_bev1-clip_bev2,anchor=TOP);
// Add teardrop profile
if (teardrop) {
ang = min(45,opp_hyp_to_ang(rmax+profmin, rmax+pmax));
xrot(-90) teardrop(l=l, r1=r1adj+profmin, r2=r2adj+profmin, ang=ang, cap_h1=r1adj+pmax, cap_h2=r2adj+pmax);
if (teardrop!=false) {
fact = is_num(teardrop) ? assert(teardrop>=0,"teardrop value cannot be negative")1-1/sqrt(2)+teardrop
: is_bool(teardrop) ? 1-1/sqrt(2)+0.05
: teardrop=="max" ? 1/sqrt(2)
: assert(false,"invalid teardrop value");
dummy = assert(fact<=1/sqrt(2), "teardrop value too large");
pdepth = pmax-profmin;
trap1 = back((r1adj+pmax)/sqrt(2),path3d(list_rotate(trapezoid(ang=45,w1 = (r1adj+pmax)*sqrt(2), h = (r1adj+pmax)*fact,anchor=FWD),1),-l/2));
trap2 = back((r2adj+pmax)/sqrt(2),path3d(list_rotate(trapezoid(ang=45,w1 = (r2adj+pmax)*sqrt(2), h = (r2adj+pmax)*fact,anchor=FWD),1), l/2));
yproj = [[1,0,0],[0,0,0],[0,0,1]];
p1a=trap1[0]+unit([0,0,-l/2]-trap1[0])*pdepth*3/4;
p1b=last(trap1)+unit([0,0,-l/2]-last(trap1))*pdepth*3/4;
p2a=trap2[0]+unit([0,0,l/2]-trap2[0])*pdepth*3/4;
p2b=last(trap2)+ unit([0,0,l/2]-last(trap2))*pdepth*3/4 ;
cut1 = reverse([p1a, p1a*yproj, p1b*yproj, p1b]);
cut2 = reverse([p2a, p2a*yproj, p2b*yproj, p2b]);
vert = [
[each cut1, each trap1],
[each cut2, each trap2]
];
vnf_polyhedron(vnf_vertex_array(vert,caps=true,col_wrap=true));
// Old code creates an internal teardrop which unfortunately doesn't print well
//ang = min(45,opp_hyp_to_ang(rmax+profmin, rmax+pmax));
//xrot(-90) teardrop(l=l, r1=r1adj+profmin, r2=r2adj+profmin, ang=ang, cap_h1=r1adj+pmax, cap_h2=r2adj+pmax);
}
}
children();
@ -2072,7 +2098,6 @@ module _nutshape(nutwidth, h, shape, bevel1, bevel2)
intersection(){
if (shape=="hex")
cyl(d=nutwidth, circum=true, $fn=6, l=h, chamfer1=bevel1?0:nutwidth*.01, chamfer2=bevel2?0:nutwidth*.01);
//vnf_polyhedron(vnf);
else
cuboid([nutwidth,nutwidth,h],chamfer=nutwidth*.01, except=[if (bevel1) BOT, if(bevel2) TOP]);
fn = quantup(segs(r=nutwidth/2),shape=="hex"?6:4);

View file

@ -9,7 +9,8 @@
//////////////////////////////////////////////////////////////////////
BOSL_VERSION = [2,0,705];
BOSL_VERSION = [2,0,710];
// Section: BOSL Library Version Functions

View file

@ -423,20 +423,29 @@ function vnf_join(vnfs) =
// Topics: VNF Generators, Lists
// See Also: vnf_tri_array(), vnf_join(), vnf_vertex_array(), vnf_from_region()
// Usage:
// vnf = vnf_from_polygons(polygons);
// vnf = vnf_from_polygons(polygons, [eps]);
// Description:
// Given a list of 3D polygons, produces a VNF containing those polygons.
// It is up to the caller to make sure that the points are in the correct order to make the face
// normals point outwards. No checking for duplicate vertices is done. If you want to
// remove duplicate vertices use {{vnf_merge_points()}}.
// remove duplicate vertices use {{vnf_merge_points()}}. Polygons with zero area are discarded from the face list by default.
// If you give non-coplanar faces an error is displayed. These checks increase run time by about 2x for triangular polygons, but
// about 10x for pentagons; the checks can be disabled by setting fast=true.
// Arguments:
// polygons = The list of 3D polygons to turn into a VNF
function vnf_from_polygons(polygons) =
// fast = Set to true to skip area and coplanarity checks for increased speed. Default: false
// eps = Polygons with area small than this are discarded. Default: EPSILON
function vnf_from_polygons(polygons,fast=false,eps=EPSILON) =
assert(is_list(polygons) && is_path(polygons[0]),"Input should be a list of polygons")
let(
offs = cumsum([0, for(p=polygons) len(p)]),
faces = [for(i=idx(polygons))
[for (j=idx(polygons[i])) offs[i]+j]
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"))
)
if (is_def(area) && area > eps)
[for (j=idx(polygons[i])) offs[i]+j]
]
)
[flatten(polygons), faces];
@ -898,6 +907,7 @@ function _vnf_sort_vertices(vnf, idx=[2,1,0]) =
// Function: vnf_slice()
// Synopsis: Slice the faces of a VNF along an axis.
// SynTags: VNF
@ -1605,34 +1615,28 @@ module _show_faces(vertices, faces, size=1, filter) {
for (i = [0:1:len(faces)-1]) {
face = faces[i];
if (face[0] < 0 || face[1] < 0 || face[2] < 0 || face[0] >= vlen || face[1] >= vlen || face[2] >= vlen) {
echo("BAD FACE: ", vlen=vlen, face=face);
} else if (is_undef(filter) || any(face,filter)) {
echo(str("INVALID FACE: indices of face ",i," are out of bounds [0,",vlen-1,"]: face=",face));
}
else if (is_undef(filter) || any(face,filter)) {
verts = select(vertices,face);
c = mean(verts);
v0 = verts[0];
v1 = verts[1];
v2 = verts[2];
dv0 = unit(v1 - v0);
dv1 = unit(v2 - v0);
nrm0 = cross(dv0, dv1);
nrm1 = UP;
axis = vector_axis(nrm0, nrm1);
ang = vector_angle(nrm0, nrm1);
theta = atan2(nrm0[1], nrm0[0]);
translate(c) {
rotate(a=180-ang, v=axis) {
zrot(theta-90)
linear_extrude(height=size/10, center=true, convexity=10) {
union() {
normal = polygon_normal(verts);
if (is_undef(normal))
echo(str("DEGENERATE FACE: face ",i," has no normal vector, face=", face));
else {
axis = vector_axis(normal, DOWN);
ang = vector_angle(normal, DOWN);
theta = atan2(normal[1], normal[0]);
translate(mean(verts))
rotate(a=(180-ang), v=axis)
zrot(theta+90)
linear_extrude(height=size/10, center=true, convexity=10) {
text(text=str(i), size=size, halign="center");
text(text=str("_"), size=size, halign="center");
}
}
}
}
}
}
}
}
}
}
@ -1734,7 +1738,7 @@ module debug_vnf(vnf, faces=true, vertices=true, opacity=0.5, size=1, convexity=
// e = [ 50,-50, 50];
// vnf = vnf_from_polygons([
// [a, b, e], [a, c, b], [a, d, c], [a, e, d], [b, c, d, e]
// ]);
// ],fast=true);
// vnf_validate(vnf);
// Example(3D,Edges): MULTCONN Errors; More Than Two Faces Attached to the Same Edge. This confuses CGAL, and can lead to failed renders.
// vnf = vnf_triangulate(linear_sweep(union(square(50), square(50,anchor=BACK+RIGHT)), height=50));
@ -1939,14 +1943,9 @@ function _vnf_validate(vnf, show_warns=true, check_isects=false) =
) hole_edges? issues :
let(
nonplanars = unique([
for (i = idx(faces)) let(
face = faces[i],
area = face_areas[i],
faceverts = [for (k=face) varr[k]]
)
if (is_num(area) && abs(area) > EPSILON)
if (!is_coplanar(faceverts))
_vnf_validate_err("NONPLANAR", face)
for (i = idx(faces))
if (is_undef(face_areas[i]))
_vnf_validate_err("NONPLANAR", faces[i])
]),
issues = concat(issues, nonplanars)
) issues;