From 76e376e28b2de8d933dddab6655410c6677d7107 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Fri, 26 Jan 2024 18:06:43 -0500 Subject: [PATCH 01/24] fix check in path_cut for cuts at endpoints --- paths.scad | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/paths.scad b/paths.scad index 7631ac4..52f18f5 100644 --- a/paths.scad +++ b/paths.scad @@ -798,8 +798,8 @@ function path_cut(path,cutdist,closed) = let(closed=default(closed,false)) assert(is_bool(closed)) assert(is_vector(cutdist)) - assert(last(cutdist)0, "Cut distances must be strictly positive") + assert(last(cutdist)EPSILON, "Cut distances must be strictly positive") let( cutlist = path_cut_points(path,cutdist,closed=closed) ) From 00ffb4aec5082d670372619389d911535a96aef6 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Fri, 26 Jan 2024 18:08:22 -0500 Subject: [PATCH 02/24] fix example --- beziers.scad | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/beziers.scad b/beziers.scad index 0f756f4..21b6bed 100644 --- a/beziers.scad +++ b/beziers.scad @@ -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)) From b59fdb61edfd40e0bed38e5901fdc9bef75384ac Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Fri, 26 Jan 2024 18:09:39 -0500 Subject: [PATCH 03/24] arc fixes: wedge point order, some better arg checks --- drawing.scad | 60 ++++++++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/drawing.scad b/drawing.scad index 85b552e..50261e9 100644 --- a/drawing.scad +++ b/drawing.scad @@ -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) From ad504a5962d78430dec7802d2fc4006f451baaf8 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Fri, 26 Jan 2024 18:10:35 -0500 Subject: [PATCH 04/24] don't pass $fn from module to function --- shapes3d.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shapes3d.scad b/shapes3d.scad index f6bc520..26026ad 100644 --- a/shapes3d.scad +++ b/shapes3d.scad @@ -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, From dd02bb42c5b37b10fb721ea851131b7b689af702 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Fri, 26 Jan 2024 19:40:00 -0500 Subject: [PATCH 05/24] filter zero area polygons in vnf_from_polygons --- vnf.scad | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/vnf.scad b/vnf.scad index 06ecff4..28f6e97 100644 --- a/vnf.scad +++ b/vnf.scad @@ -423,20 +423,23 @@ 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. // Arguments: // polygons = The list of 3D polygons to turn into a VNF -function vnf_from_polygons(polygons) = +// eps = Polygons with area small than this are discarded. Default: EPSILON +function vnf_from_polygons(polygons,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=polygon_area(polygons[i])) + if (is_def(area) && area > eps) + [for (j=idx(polygons[i])) offs[i]+j] ] ) [flatten(polygons), faces]; From 10727a26b660fd6c01d496926499823786a4cfcb Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Fri, 26 Jan 2024 20:10:51 -0500 Subject: [PATCH 06/24] ring() --- shapes2d.scad | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/shapes2d.scad b/shapes2d.scad index 772314f..477b355 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -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) || num_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. From 0149443601d947618962cc5d568f4e65f2cf4299 Mon Sep 17 00:00:00 2001 From: adrianVmariano Date: Sat, 27 Jan 2024 01:11:19 +0000 Subject: [PATCH 07/24] Version Bump --- version.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.scad b/version.scad index f1863ed..5605ae8 100644 --- a/version.scad +++ b/version.scad @@ -9,7 +9,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,699]; +BOSL_VERSION = [2,0,700]; // Section: BOSL Library Version Functions From 825d0641ac7de4e44aae2f380d10791d3744b923 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sat, 27 Jan 2024 09:44:54 -0500 Subject: [PATCH 08/24] add error for non-coplanar and fast option to vnf_from_polygons --- vnf.scad | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/vnf.scad b/vnf.scad index 28f6e97..22b4990 100644 --- a/vnf.scad +++ b/vnf.scad @@ -428,16 +428,22 @@ function vnf_join(vnfs) = // 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()}}. Polygons with zero area are discarded from the face list by default. +// 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 +// 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,eps=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)) - let(area=polygon_area(polygons[i])) + 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] ] From bd5fd65e3d4545f4938a6d076c7e0b1a4ebf78c4 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sat, 27 Jan 2024 09:46:08 -0500 Subject: [PATCH 09/24] remove noncoplanar polygon fron vnf_from_polygon test --- tests/test_vnf.scad | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_vnf.scad b/tests/test_vnf.scad index a344e6a..9da479f 100644 --- a/tests/test_vnf.scad +++ b/tests/test_vnf.scad @@ -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(); From d6fe226fcae125fe7028d5efaa7d2dae16577be8 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sat, 27 Jan 2024 10:52:54 -0500 Subject: [PATCH 10/24] Fix teardrop for threaded screws --- affine.scad | 3 --- rounding.scad | 2 +- screws.scad | 30 ++++++++++++++++++------------ shapes2d.scad | 2 +- threading.scad | 45 +++++++++++++++++++++++++++++++++++---------- 5 files changed, 55 insertions(+), 27 deletions(-) diff --git a/affine.scad b/affine.scad index 78684d6..ca94cf9 100644 --- a/affine.scad +++ b/affine.scad @@ -450,9 +450,6 @@ function affine3d_rot_from_to(from, to) = ]; - - - // Function: affine3d_mirror() // Synopsis: Returns a 3D (4x4) reflection transformation matrix. // SynTags: Mat diff --git a/rounding.scad b/rounding.scad index 25919c1..75365cb 100644 --- a/rounding.scad +++ b/rounding.scad @@ -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)) ]) ) diff --git a/screws.scad b/screws.scad index 7dfa703..172ff07 100644 --- a/screws.scad +++ b/screws.scad @@ -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) { diff --git a/shapes2d.scad b/shapes2d.scad index 477b355..20d8920 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -1603,7 +1603,7 @@ function ring(n,ring_width,r,r1,r2,angle,d,d1,d2,cp,points,corner, width,thickne full = is_def(angle) ? false : full ) assert(is_undef(start) || is_def(angle), "start requires angle") - assert(is_undef(angle) || num_defined([thickness,width,points,corner]), "Cannot give angle with points, corner, width or thickness") + 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") diff --git a/threading.scad b/threading.scad index efd4af3..5a747a0 100644 --- a/threading.scad +++ b/threading.scad @@ -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); From 3c8e76f65dd4134c514bea9a0bd7ebf49bc281ad Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sat, 27 Jan 2024 12:12:32 -0500 Subject: [PATCH 11/24] change skin to use different algorithm for "incremental" that is supposed to be more accurate. --- skin.scad | 71 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/skin.scad b/skin.scad index edf5d91..9f6a7f2 100644 --- a/skin.scad +++ b/skin.scad @@ -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 = iEPSILON) - ) - 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") @@ -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) = @@ -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 : From 5779f62514c82bba292c6dcdf1757ef7a190c5c6 Mon Sep 17 00:00:00 2001 From: adrianVmariano Date: Sat, 27 Jan 2024 17:17:05 +0000 Subject: [PATCH 12/24] Version Bump --- version.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.scad b/version.scad index 5605ae8..13ecfba 100644 --- a/version.scad +++ b/version.scad @@ -9,7 +9,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,700]; +BOSL_VERSION = [2,0,701]; // Section: BOSL Library Version Functions From 1be22e4c3a97e9f6e9bff73c5ce8bea8220a3a95 Mon Sep 17 00:00:00 2001 From: adrianVmariano Date: Sat, 27 Jan 2024 17:50:24 +0000 Subject: [PATCH 13/24] Version Bump --- version.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.scad b/version.scad index 496cd7b..c5fc62f 100644 --- a/version.scad +++ b/version.scad @@ -10,7 +10,7 @@ -BOSL_VERSION = [2,0,703]; +BOSL_VERSION = [2,0,704]; // Section: BOSL Library Version Functions From 4e628549095466a9ee19bd4efeb40c0ca55c9a96 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sat, 27 Jan 2024 19:39:16 -0500 Subject: [PATCH 14/24] fix vnf_validate to detect non-coplanar; fix docs --- vnf.scad | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/vnf.scad b/vnf.scad index 22b4990..9dcbcb0 100644 --- a/vnf.scad +++ b/vnf.scad @@ -1743,7 +1743,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)); @@ -1948,14 +1948,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; From 37259b9a7a6f05a23d63b61eb578ddf959785d4f Mon Sep 17 00:00:00 2001 From: adrianVmariano Date: Sun, 28 Jan 2024 00:39:46 +0000 Subject: [PATCH 15/24] Version Bump --- version.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.scad b/version.scad index c5fc62f..fffe95f 100644 --- a/version.scad +++ b/version.scad @@ -10,7 +10,7 @@ -BOSL_VERSION = [2,0,704]; +BOSL_VERSION = [2,0,705]; // Section: BOSL Library Version Functions From 6725f7311b968931c8a43cdf06f07a0410458e14 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sat, 27 Jan 2024 22:25:25 -0500 Subject: [PATCH 16/24] only $fn affects texture, not $fa or $fs --- skin.scad | 10 +++++----- vnf.scad | 1 - 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/skin.scad b/skin.scad index 9f6a7f2..cb08842 100644 --- a/skin.scad +++ b/skin.scad @@ -2969,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, @@ -3011,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, @@ -3374,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) [ @@ -3429,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( diff --git a/vnf.scad b/vnf.scad index 9dcbcb0..5ede45e 100644 --- a/vnf.scad +++ b/vnf.scad @@ -906,7 +906,6 @@ function _vnf_sort_vertices(vnf, idx=[2,1,0]) = ) sorted_vnf; - // Function: vnf_slice() // Synopsis: Slice the faces of a VNF along an axis. // SynTags: VNF From 0048336ad3e33a3a617cd96c62453c2be343e55f Mon Sep 17 00:00:00 2001 From: adrianVmariano Date: Sun, 28 Jan 2024 03:26:02 +0000 Subject: [PATCH 17/24] Version Bump --- version.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.scad b/version.scad index fffe95f..977e99b 100644 --- a/version.scad +++ b/version.scad @@ -10,7 +10,7 @@ -BOSL_VERSION = [2,0,705]; +BOSL_VERSION = [2,0,706]; // Section: BOSL Library Version Functions From 75f7a75ac2d2c1bae7bce189177c1e2a0f62ad95 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sun, 28 Jan 2024 14:26:40 -0500 Subject: [PATCH 18/24] fix debug_vnf for case with collinear edges in a face --- vnf.scad | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/vnf.scad b/vnf.scad index 5ede45e..7bd8d01 100644 --- a/vnf.scad +++ b/vnf.scad @@ -906,6 +906,8 @@ function _vnf_sort_vertices(vnf, idx=[2,1,0]) = ) sorted_vnf; + + // Function: vnf_slice() // Synopsis: Slice the faces of a VNF along an axis. // SynTags: VNF @@ -1613,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"); - } - } - } + } } } } - } + } } From 64de46b484deb6294f5b7e46b40d2edbe3b6de8e Mon Sep 17 00:00:00 2001 From: adrianVmariano Date: Sun, 28 Jan 2024 19:27:16 +0000 Subject: [PATCH 19/24] Version Bump --- version.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.scad b/version.scad index 977e99b..b80ea6c 100644 --- a/version.scad +++ b/version.scad @@ -10,7 +10,7 @@ -BOSL_VERSION = [2,0,706]; +BOSL_VERSION = [2,0,707]; // Section: BOSL Library Version Functions From 5435bb1fc27b4ea959a3b23083e99b975af0ff87 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sun, 28 Jan 2024 18:22:04 -0500 Subject: [PATCH 20/24] add deduplicate to rect(), add deduplicate to is_path_simple add epsilon to degenerate bezier patch detection --- beziers.scad | 4 ++-- paths.scad | 5 ++++- shapes2d.scad | 2 +- tests/test_paths.scad | 8 +++++++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/beziers.scad b/beziers.scad index 21b6bed..605ce5e 100644 --- a/beziers.scad +++ b/beziers.scad @@ -1252,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], diff --git a/paths.scad b/paths.scad index 52f18f5..3d3b1e8 100644 --- a/paths.scad +++ b/paths.scad @@ -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], diff --git a/shapes2d.scad b/shapes2d.scad index 20d8920..fcf17f4 100644 --- a/shapes2d.scad +++ b/shapes2d.scad @@ -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]]] diff --git a/tests/test_paths.scad b/tests/test_paths.scad index cf8d740..f86c35f 100644 --- a/tests/test_paths.scad +++ b/tests/test_paths.scad @@ -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))); +} + From 84edc990cbaf683da44dd9d4a96becb27ae6997c Mon Sep 17 00:00:00 2001 From: adrianVmariano Date: Sun, 28 Jan 2024 23:23:02 +0000 Subject: [PATCH 21/24] Version Bump --- version.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.scad b/version.scad index b80ea6c..5a1bb5f 100644 --- a/version.scad +++ b/version.scad @@ -10,7 +10,7 @@ -BOSL_VERSION = [2,0,707]; +BOSL_VERSION = [2,0,708]; // Section: BOSL Library Version Functions From 2dedff5e60499e7f10a19438f75aabdf960fec82 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Sun, 28 Jan 2024 23:05:19 -0500 Subject: [PATCH 22/24] doc fixes --- attachments.scad | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/attachments.scad b/attachments.scad index ebd8a4a..c96e27b 100644 --- a/attachments.scad +++ b/attachments.scad @@ -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. From ea5eda65b92e7a54fc6ebec93273c472e453e910 Mon Sep 17 00:00:00 2001 From: adrianVmariano Date: Mon, 29 Jan 2024 04:18:57 +0000 Subject: [PATCH 23/24] Version Bump --- version.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.scad b/version.scad index 5a1bb5f..3a93162 100644 --- a/version.scad +++ b/version.scad @@ -10,7 +10,7 @@ -BOSL_VERSION = [2,0,708]; +BOSL_VERSION = [2,0,709]; // Section: BOSL Library Version Functions From ca2f8677fdd1b2dbb0b6f6876dfce3c10c05c277 Mon Sep 17 00:00:00 2001 From: revarbat Date: Thu, 1 Feb 2024 23:22:28 +0000 Subject: [PATCH 24/24] Version Bump --- version.scad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.scad b/version.scad index 3a93162..c3eba25 100644 --- a/version.scad +++ b/version.scad @@ -10,7 +10,7 @@ -BOSL_VERSION = [2,0,709]; +BOSL_VERSION = [2,0,710]; // Section: BOSL Library Version Functions