From c465a5b746746b79ff2dad405ddb92221fd55ad8 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Sun, 16 Aug 2020 23:09:59 -0700 Subject: [PATCH 1/5] cuboid() with negative chamfer had mispositioned anti-chamfers. --- shapes.scad | 4 ++-- version.scad | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shapes.scad b/shapes.scad index cb6d4e5..d612b0a 100644 --- a/shapes.scad +++ b/shapes.scad @@ -102,8 +102,8 @@ module cuboid( if (edges == EDGES_ALL && trimcorners) { if (chamfer<0) { cube(size, center=true) { - attach(TOP) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); - attach(BOT) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); + attach(TOP,overlap=0) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); + attach(BOT,overlap=0) prismoid([size.x,size.y], [size.x-2*chamfer,size.y-2*chamfer], h=-chamfer, anchor=TOP); } } else { isize = [for (v = size) max(0.001, v-2*chamfer)]; diff --git a/version.scad b/version.scad index e6ba60b..21ab73b 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,402]; +BOSL_VERSION = [2,0,403]; // Section: BOSL Library Version Functions From 25841bda7328edcaf0f82387131bfd84149cf5b2 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Sun, 16 Aug 2020 23:10:43 -0700 Subject: [PATCH 2/5] Fixed attachable() anchors for vnf's with extent=false. --- attachments.scad | 35 +++++++++++++---------------------- version.scad | 2 +- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/attachments.scad b/attachments.scad index 6004e29..2ac4367 100644 --- a/attachments.scad +++ b/attachments.scad @@ -458,10 +458,9 @@ function find_anchor(anchor, geom) = eps = 1/2048, points = vnf[0], faces = vnf[1], - rpts = rot(from=anchor, to=RIGHT, p=move(point3d(-cp), p=points)), + rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), points), hits = [ - for (i = idx(faces)) let( - face = faces[i], + for (face = faces) let( verts = select(rpts, face) ) if ( max(subindex(verts,0)) >= -eps && @@ -470,35 +469,27 @@ function find_anchor(anchor, geom) = min(subindex(verts,1)) <= eps && min(subindex(verts,2)) <= eps ) let( - pt = polygon_line_intersection( - select(points, face), - [CENTER,anchor], eps=eps - ) - ) if (!is_undef(pt)) [norm(pt), i, pt] + poly = select(points, face), + pt = polygon_line_intersection(poly, [cp,cp+anchor], bounded=[true,false], eps=eps) + ) if (!is_undef(pt)) let( + plane = plane_from_polygon(poly), + n = plane_normal(plane) + ) + [norm(pt-cp), n, pt] ] ) assert(len(hits)>0, "Anchor vector does not intersect with the shape. Attachment failed.") let( furthest = max_index(subindex(hits,0)), - pos = point3d(cp) + hits[furthest][2], dist = hits[furthest][0], - nfaces = [for (hit = hits) if(approx(hit[0],dist,eps=eps)) hit[1]], - n = unit( - sum([ - for (i = nfaces) let( - faceverts = select(points, faces[i]), - faceplane = plane_from_points(faceverts), - nrm = plane_normal(faceplane) - ) nrm - ]) / len(nfaces), - UP - ) + pos = hits[furthest][2], + n = unit(sum([for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]])) ) [anchor, pos, n, oang] ) : type == "vnf_extent"? ( //vnf let( vnf=geom[1], - rpts = rot(from=anchor, to=RIGHT, p=move(point3d(-cp), p=vnf[0])), + rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), vnf[0]), maxx = max(subindex(rpts,0)), idxs = [for (i = idx(rpts)) if (approx(rpts[i].x, maxx)) i], mm = pointlist_bounds(select(rpts,idxs)), @@ -849,7 +840,7 @@ module attachable( // Module: position() // Usage: -// position(from, [overlap]) ... +// position(from) ... // Description: // Attaches children to a parent object at an anchor point. // Arguments: diff --git a/version.scad b/version.scad index 21ab73b..85cad34 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,403]; +BOSL_VERSION = [2,0,404]; // Section: BOSL Library Version Functions From 2ff93f34cdf49672f34d08d0983309f621507296 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Mon, 17 Aug 2020 18:43:58 -0400 Subject: [PATCH 3/5] Added attachment support to all modules, and bug fixed rounded_prism (problem introduced by error checking in det2), and fixed broken example in rounding. --- rounding.scad | 286 ++++++++++++++++++++++++++++---------------------- skin.scad | 63 ++++++++--- 2 files changed, 208 insertions(+), 141 deletions(-) diff --git a/rounding.scad b/rounding.scad index 254d035..b3bfa45 100644 --- a/rounding.scad +++ b/rounding.scad @@ -458,10 +458,10 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals -// Module: offset_sweep() +// Function&Module: offset_sweep() // // Description: -// Takes a 2d path as input and extrudes it upwards and/or downward. Each layer in the extrusion is produced using `offset()` to expand or shrink the previous layer. +// Takes a 2d path as input and extrudes it upwards and/or downward. Each layer in the extrusion is produced using `offset()` to expand or shrink the previous layer. When invoked as a function returns a VNF; when invoked as a module produces geometry. // You can specify a sequence of offsets values, or you can use several built-in offset profiles that are designed to provide end treatments such as roundovers. // The path is shifted by `offset()` multiple times in sequence // to produce the final shape (not multiple shifts from one parent), so coarse definition of the input path will degrade @@ -543,8 +543,12 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals // angle = default angle for chamfers. Default: 45 // joint = default joint value for smooth roundover. // k = default curvature parameter value for "smooth" roundover -// convexity = convexity setting for use with polyhedron. Default: 10 -// +// convexity = convexity setting for use with polyhedron. (module only) Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: Rounding a star shaped prism with postive radius values // star = star(5, r=22, ir=13); // rounded_star = round_corners(star, cut=flatten(repeat([.5,0],5)), $fn=24); @@ -650,118 +654,118 @@ function smooth_path(path, tangents, size, relsize, splinesteps=10, uniform=fals // up(1) // offset_sweep(offset(rhex,r=-1), height=9.5, bottom=os_circle(r=2), top=os_teardrop(r=-4)); // } -module offset_sweep( - path, height, h, l, - top=[], bottom=[], - offset="round", r=0, steps=16, - quality=1, check_valid=true, - offset_maxstep=1, extra=0, - cut=undef, chamfer_width=undef, chamfer_height=undef, - joint=undef, k=0.75, angle=45, - convexity=10 -) { - // This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce - // the inputs for the polyhedron module. - function make_polyhedron(path,offsets, offset_type, flip_faces, quality, check_valid, maxstep, offsetind=0, vertexcount=0, vertices=[], faces=[] )= - offsetind==len(offsets)? ( - let( - bottom = list_range(n=len(path),s=vertexcount), - oriented_bottom = !flip_faces? bottom : reverse(bottom) - ) [vertices, concat(faces,[oriented_bottom])] - ) : ( - let( - this_offset = offsetind==0? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0], - delta = offset_type=="delta" || offset_type=="chamfer" ? this_offset : undef, - r = offset_type=="round"? this_offset : undef, - do_chamfer = offset_type == "chamfer" + + +// This function does the actual work of repeatedly calling offset() and concatenating the resulting face and vertex lists to produce +// the inputs for the polyhedron module. +function _make_offset_polyhedron(path,offsets, offset_type, flip_faces, quality, check_valid, maxstep, offsetind=0, + vertexcount=0, vertices=[], faces=[] )= + offsetind==len(offsets)? ( + let( + bottom = list_range(n=len(path),s=vertexcount), + oriented_bottom = !flip_faces? bottom : reverse(bottom) + ) [vertices, concat(faces,[oriented_bottom])] + ) : ( + let( + this_offset = offsetind==0? offsets[0][0] : offsets[offsetind][0] - offsets[offsetind-1][0], + delta = offset_type=="delta" || offset_type=="chamfer" ? this_offset : undef, + r = offset_type=="round"? this_offset : undef, + do_chamfer = offset_type == "chamfer" + ) + let( + vertices_faces = offset( + path, r=r, delta=delta, chamfer = do_chamfer, closed=true, + check_valid=check_valid, quality=quality, + maxstep=maxstep, return_faces=true, + firstface_index=vertexcount, + flip_faces=flip_faces ) - assert(num_defined([r,delta])==1,"Must set `offset` to \"round\" or \"delta") - let( - vertices_faces = offset( - path, r=r, delta=delta, chamfer = do_chamfer, closed=true, - check_valid=check_valid, quality=quality, - maxstep=maxstep, return_faces=true, - firstface_index=vertexcount, - flip_faces=flip_faces - ) - ) - make_polyhedron( - vertices_faces[0], offsets, offset_type, - flip_faces, quality, check_valid, maxstep, - offsetind+1, vertexcount+len(path), - vertices=concat( - vertices, - zip(vertices_faces[0],repeat(offsets[offsetind][1],len(vertices_faces[0]))) - ), - faces=concat(faces, vertices_faces[1]) - ) - ); - - - argspec = [ - ["r",r], - ["extra",extra], - ["type","circle"], - ["check_valid",check_valid], - ["quality",quality], - ["offset_maxstep", offset_maxstep], - ["steps",steps], - ["offset",offset], - ["chamfer_width",chamfer_width], - ["chamfer_height",chamfer_height], - ["angle",angle], - ["cut",cut], - ["joint",joint], - ["k", k], - ["points", []], - ]; - - path = check_and_fix_path(path, [2], closed=true); - clockwise = polygon_is_clockwise(path); - - top = struct_set(argspec, top, grow=false); - bottom = struct_set(argspec, bottom, grow=false); - - // This code does not work. It hits the error in make_polyhedron from offset being wrong - // before this code executes. Had to move the test into make_polyhedron, which is ugly since it's in the loop - //offsetsok = in_list(struct_val(top, "offset"),["round","delta"]) && - // in_list(struct_val(bottom, "offset"),["round","delta"]); - //assert(offsetsok,"Offsets must be one of \"round\" or \"delta\""); - - - offsets_bot = _rounding_offsets(bottom, -1); - offsets_top = _rounding_offsets(top, 1); - - if (offset == "chamfer" && (len(offsets_bot)>5 || len(offsets_top)>5)) { - echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested many layers. This can be slow or run out of recursion depth."); - } - // "Extra" height enlarges the result beyond the requested height, so subtract it - bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra"); - top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra"); - - height = get_height(l=l,h=h,height=height,dflt=bottom_height+top_height); - assert(height>=0, "Height must be nonnegative"); - - middle = height-bottom_height-top_height; - assert( - middle>=0, str( - "Specified end treatments (bottom height = ",bottom_height, - " top_height = ",top_height,") are too large for extrusion height (",height,")" + ) + _make_offset_polyhedron( + vertices_faces[0], offsets, offset_type, + flip_faces, quality, check_valid, maxstep, + offsetind+1, vertexcount+len(path), + vertices=concat( + vertices, + zip(vertices_faces[0],repeat(offsets[offsetind][1],len(vertices_faces[0]))) + ), + faces=concat(faces, vertices_faces[1]) ) ); - initial_vertices_bot = path3d(path); - vertices_faces_bot = make_polyhedron( + +function offset_sweep( + path, height, h, l, + top=[], bottom=[], + offset="round", r=0, steps=16, + quality=1, check_valid=true, + offset_maxstep=1, extra=0, + cut=undef, chamfer_width=undef, chamfer_height=undef, + joint=undef, k=0.75, angle=45 + ) = + let( + argspec = [ + ["r",r], + ["extra",extra], + ["type","circle"], + ["check_valid",check_valid], + ["quality",quality], + ["offset_maxstep", offset_maxstep], + ["steps",steps], + ["offset",offset], + ["chamfer_width",chamfer_width], + ["chamfer_height",chamfer_height], + ["angle",angle], + ["cut",cut], + ["joint",joint], + ["k", k], + ["points", []], + ], + path = check_and_fix_path(path, [2], closed=true), + clockwise = polygon_is_clockwise(path), + + top = struct_set(argspec, top, grow=false), + bottom = struct_set(argspec, bottom, grow=false), + + // This code does not work. It hits the error in _make_offset_polyhedron from offset being wrong + // before this code executes. Had to move the test into _make_offset_polyhedron, which is ugly since it's in the loop + offsetsok = in_list(struct_val(top, "offset"),["round","delta"]) + && in_list(struct_val(bottom, "offset"),["round","delta"]) + ) + assert(offsetsok,"Offsets must be one of \"round\" or \"delta\"") + let( + offsets_bot = _rounding_offsets(bottom, -1), + offsets_top = _rounding_offsets(top, 1), + dummy = offset == "chamfer" && (len(offsets_bot)>5 || len(offsets_top)>5) + ? echo("WARNING: You have selected offset=\"chamfer\", which leads to exponential growth in the vertex count and requested more than 5 layers. This can be slow or run out of recursion depth.") + : 0, + + // "Extra" height enlarges the result beyond the requested height, so subtract it + bottom_height = len(offsets_bot)==0 ? 0 : abs(select(offsets_bot,-1)[1]) - struct_val(bottom,"extra"), + top_height = len(offsets_top)==0 ? 0 : abs(select(offsets_top,-1)[1]) - struct_val(top,"extra"), + + height = get_height(l=l,h=h,height=height,dflt=bottom_height+top_height), + middle = height-bottom_height-top_height + ) + assert(height>=0, "Height must be nonnegative") + assert(middle>=0, str("Specified end treatments (bottom height = ",bottom_height, + " top_height = ",top_height,") are too large for extrusion height (",height,")" + ) + ) + let( + initial_vertices_bot = path3d(path), + + vertices_faces_bot = _make_offset_polyhedron( path, offsets_bot, struct_val(bottom,"offset"), clockwise, struct_val(bottom,"quality"), struct_val(bottom,"check_valid"), struct_val(bottom,"offset_maxstep"), vertices=initial_vertices_bot - ); + ), - top_start_ind = len(vertices_faces_bot[0]); - initial_vertices_top = zip(path, repeat(middle,len(path))); - vertices_faces_top = make_polyhedron( + top_start_ind = len(vertices_faces_bot[0]), + initial_vertices_top = zip(path, repeat(middle,len(path))), + vertices_faces_top = _make_offset_polyhedron( path, move(p=offsets_top,[0,middle]), struct_val(top,"offset"), !clockwise, struct_val(top,"quality"), @@ -769,20 +773,39 @@ module offset_sweep( struct_val(top,"offset_maxstep"), vertexcount=top_start_ind, vertices=initial_vertices_top - ); + ), middle_faces = middle==0 ? [] : [ for(i=[0:len(path)-1]) let( oneface=[i, (i+1)%len(path), top_start_ind+(i+1)%len(path), top_start_ind+i] ) !clockwise ? reverse(oneface) : oneface - ]; - up(bottom_height) { - polyhedron( - concat(vertices_faces_bot[0],vertices_faces_top[0]), - faces=concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces), - convexity=convexity - ); - } -} + ] + ) + [up(bottom_height, concat(vertices_faces_bot[0],vertices_faces_top[0])), // Vertices + concat(vertices_faces_bot[1], vertices_faces_top[1], middle_faces)]; // Faces + + +module offset_sweep(path, height, h, l, + top=[], bottom=[], + offset="round", r=0, steps=16, + quality=1, check_valid=true, + offset_maxstep=1, extra=0, + cut=undef, chamfer_width=undef, chamfer_height=undef, + joint=undef, k=0.75, angle=45, + convexity=10,anchor="origin",cp, + spin=0, orient=UP, extent=false) +{ + vnf = offset_sweep(path=path, height=height, h=h, l=l, top=top, bottom=bottom, offset=offset, r=0, steps=steps, + quality=quality, check_valid=true, offset_maxstep=1, extra=0, cut=cut, chamfer_width=chamfer_width, + chamfer_height=chamfer_height, joint=joint, k=k, angle=angle); + + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } +} + + function os_circle(r,cut,extra,check_valid, quality,steps, offset_maxstep, offset) = assert(num_defined([r,cut])==1, "Must define exactly one of `r` and `cut`") @@ -924,7 +947,6 @@ function os_profile(points, extra,check_valid, quality, offset_maxstep, offset) // joint = default joint value for smooth roundover. // k = default curvature parameter value for "smooth" roundover // convexity = convexity setting for use with polyhedron. Default: 10 -// // Example: Chamfered elliptical prism. If you stretch a chamfered cylinder the chamfer will be uneven. // convex_offset_extrude(bottom = os_chamfer(height=-2), top=os_chamfer(height=1), height=7) // xscale(4)circle(r=6,$fn=64); @@ -1364,8 +1386,6 @@ module offset_stroke(path, width=1, rounded=true, start, end, check_valid=true, } } - - function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = let( N = len(top), @@ -1395,8 +1415,8 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = let( prev_corner = prev_offset + abs(rtop_in)*in_prev, next_corner = next_offset + abs(rtop_in)*in_next, - prev_degenerate = is_undef(ray_intersection([far_corner, far_corner+prev], [prev_offset, prev_offset+in_prev])), - next_degenerate = is_undef(ray_intersection([far_corner, far_corner+next], [next_offset, next_offset+in_next])) + prev_degenerate = is_undef(ray_intersection(path2d([far_corner, far_corner+prev]), path2d([prev_offset, prev_offset+in_prev]))), + next_degenerate = is_undef(ray_intersection(path2d([far_corner, far_corner+next]), path2d([next_offset, next_offset+in_next]))) ) [ prev_degenerate ? far_corner : prev_corner, far_corner, @@ -1452,6 +1472,11 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = // splinesteps = number of segments to use for curved patches. Default: 16 // debug = turn on debug mode which displays illegal polyhedra and shows the bezier corner patches for troubleshooting purposes. Default: False // convexity = convexity parameter for polyhedron(), only for module version. Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: Uniformly rounded pentagonal prism // rounded_prism(pentagon(3), height=3, joint_top=0.5, joint_bot=0.5, joint_sides=0.5); // Example: Maximum possible rounding. @@ -1500,15 +1525,21 @@ function _rp_compute_patches(top, bot, rtop, rsides, ktop, ksides, concave) = // rounded_prism(apply(yrot(95),path3d(hexagon(3))), apply(yrot(95), path3d(hexagon(3),3)), joint_top=2, joint_bot=1, joint_sides=1); module rounded_prism(bottom, top, joint_bot, joint_top, joint_sides, k_bot, k_top, k_sides, - k=0.5, splinesteps=16, h, length, l, height, convexity=10, debug=false) + k=0.5, splinesteps=16, h, length, l, height, convexity=10, debug=false, + anchor="origin",cp,spin=0, orient=UP, extent=false) { result = rounded_prism(bottom=bottom, top=top, joint_bot=joint_bot, joint_top=joint_top, joint_sides=joint_sides, k_bot=k_bot, k_top=k_top, k_sides=k_sides, k=k, splinesteps=splinesteps, h=h, length=length, height=height, l=l,debug=debug); - if (debug){ - vnf_polyhedron(result[1], convexity=convexity); - trace_bezier_patches(result[0], showcps=true, splinesteps=splinesteps, $fn=16, showdots=false, showpatch=false); + vnf = debug ? result[1] : result; + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + if (debug){ + vnf_polyhedron(vnf, convexity=convexity); + trace_bezier_patches(result[0], showcps=true, splinesteps=splinesteps, $fn=16, showdots=false, showpatch=false); + } + else vnf_polyhedron(vnf,convexity=convexity); + children(); } - else vnf_polyhedron(result,convexity=convexity); } @@ -1880,7 +1911,7 @@ function _circle_mask(r) = // $fn=128; // difference(){ // tube(or=r, wall=2, h=45); -// bent_cutout_mask(r-1, 2.1, apply(back(15),subdivide_path(round_corners(star(n=7,ir=5,or=10), cut=flatten(repeat([0.5,0],7))),14*15,closed=true))); +// bent_cutout_mask(r-1, 2.1, apply(back(15),subdivide_path(round_corners(star(n=7,ir=5,or=10), cut=flatten(repeat([0.5,0],7)),$fn=32),14*15,closed=true))); // } // } // Example(2D): Cutting a slot in a cylinder is tricky if you want rounded corners at the top. This slot profile has slightly angled top edges to blend into the top edge of the cylinder. @@ -1944,6 +1975,7 @@ function _circle_mask(r) = module bent_cutout_mask(r, thickness, path, convexity=10) { + no_children($children); assert(is_path(path,2),"Input path must be a 2d path") assert(r-thickness>0, "Thickness too large for radius"); assert(thickness>0, "Thickness must be positive"); diff --git a/skin.scad b/skin.scad index 30d8a17..87ae8d4 100644 --- a/skin.scad +++ b/skin.scad @@ -16,7 +16,8 @@ include // Function&Module: skin() // Usage: As module: -// skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]); +// skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z], [convexity], +// [anchor],[cp],[spin],[orient],[extent]); // Usage: As function: // vnf = skin(profiles, [slices], [refine], [method], [sampling], [caps], [closed], [z]); // Description: @@ -117,6 +118,12 @@ include // caps = true to create endcap faces when closed is false. Can be a length 2 boolean array. Default is true if closed is false. // method = method for connecting profiles, one of "distance", "tangent", "direct" or "reindex". Default: "direct". // z = array of height values for each profile if the profiles are 2d +// convexity = convexity setting for use with polyhedron. (module only) Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: // skin([octagon(4), circle($fn=70,r=2)], z=[0,3], slices=10); // Example: Rotating the pentagon place the zero index at different locations, giving a twist @@ -315,11 +322,15 @@ include // stroke(zrot(30, p=yscale(0.5, p=circle(d=120))),width=10,closed=true); // } // } - - -module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, convexity=10) +module skin(profiles, slices, refine=1, method="direct", sampling, caps, closed=false, z, convexity=10, + anchor="origin",cp,spin=0, orient=UP, extent=false) { - vnf_polyhedron(skin(profiles, slices, refine, method, sampling, caps, closed, z), convexity=convexity); + vnf = skin(profiles, slices, refine, method, sampling, caps, closed, z); + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } } @@ -803,6 +814,12 @@ function associate_vertices(polygons, split, curpoly=0) = // transformations = list of 4x4 matrices to apply // closed = set to true to form a closed (torus) model. Default: false // caps = true to create endcap faces when closed is false. Can be a singe boolean to specify endcaps at both ends, or a length 2 boolean array. Default is true if closed is false. +// convexity = convexity setting for use with polyhedron. (module only) Default: 10 +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // Example: This is the "sweep-drop" example from list-comprehension-demos. // function drop(t) = 100 * 0.5 * (1 - cos(180 * t)) * sin(180 * t) + 1; // function path(t) = [0, 0, 80 + 80 * cos(180 * t)]; @@ -839,9 +856,16 @@ function sweep(shape, transformations, closed=false, caps) = assert(!closed || !caps, "Cannot make closed shape with caps") _skin_core([for(i=[0:len(transformations)-(closed?0:1)]) apply(transformations[i%len(transformations)],path3d(shape))],caps=fullcaps); -module sweep(shape, transformations, closed=false, caps, convexity=10) { - vnf_polyhedron(sweep(shape, transformations, closed, caps), convexity=convexity); -} +module sweep(shape, transformations, closed=false, caps, convexity=10, + anchor="origin",cp,spin=0, orient=UP, extent=false) +{ + vnf = sweep(shape, transformations, closed, caps); + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } +} // Function&Module: path_sweep() @@ -906,8 +930,13 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) { // tangent = a list of tangent vectors in case you need more accuracy (particularly at the end points of your curve) // relaxed = set to true with the "manual" method to relax the orthogonality requirement of cross sections to the path tangent. Default: false // caps = Can be a boolean or vector of two booleans. Set to false to disable caps at the two ends. Default: true +// transforms = set to true to return transforms instead of a VNF. These transforms can be manipulated and passed to sweep(). Default: false. // convexity = convexity parameter for polyhedron(). Only accepted by the module version. Default: 10 -// transforms = set to true to return transforms instead of a VNF. These transforms can be manipulated and passed to sweep(). Default: false. +// anchor = Translate so anchor point is at the origin. (module only) Default: "origin" +// spin = Rotate this many degrees around Z axis after anchor. (module only) Default: 0 +// orient = Vector to rotate top towards after spin (module only) +// extent = use extent method for computing anchors. (module only) Default: false +// cp = set centerpoint for anchor computation. (module only) Default: object centroid // // Example(2D): We'll use this shape in several examples // ushape = [[-10, 0],[-10, 10],[ -7, 10],[ -7, 2],[ 7, 2],[ 7, 7],[ 10, 7],[ 10, 0]]; @@ -1121,13 +1150,19 @@ module sweep(shape, transformations, closed=false, caps, convexity=10) { // outside = [for(i=[0:len(trans)-1]) trans[i]*scale(lerp(1,1.5,i/(len(trans)-1)))]; // inside = [for(i=[len(trans)-1:-1:0]) trans[i]*scale(lerp(1.1,1.4,i/(len(trans)-1)))]; // sweep(shape, concat(outside,inside),closed=true); - module path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, - symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10) + symmetry=1, last_normal, tangent, relaxed=false, caps, convexity=10, + anchor="origin",cp,spin=0, orient=UP, extent=false) { - vnf_polyhedron(path_sweep(shape, path, method, normal, closed, twist, twist_by_length, - symmetry, last_normal, tangent, relaxed, caps), convexity=convexity); -} + vnf = path_sweep(shape, path, method, normal, closed, twist, twist_by_length, + symmetry, last_normal, tangent, relaxed, caps); + attachable(anchor=anchor, spin=spin, orient=orient, vnf=vnf, extent=extent, cp=is_def(cp) ? cp : vnf_centroid(vnf)) + { + vnf_polyhedron(vnf,convexity=convexity); + children(); + } +} + function path_sweep(shape, path, method="incremental", normal, closed=false, twist=0, twist_by_length=true, symmetry=1, last_normal, tangent, relaxed=false, caps, transforms=false) = From 9f9c128b6c8a2e5714d21f64df2a6159bff1ded0 Mon Sep 17 00:00:00 2001 From: Adrian Mariano Date: Mon, 17 Aug 2020 20:44:42 -0400 Subject: [PATCH 4/5] Added rabbit clips. --- joiners.scad | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) diff --git a/joiners.scad b/joiners.scad index 1ab34e6..1178d4e 100644 --- a/joiners.scad +++ b/joiners.scad @@ -564,6 +564,7 @@ module dovetail(gender, length, l, width, w, height, h, angle, slope, taper, bac } +// Section: Tension Clips // h is total height above 0 of the nub // nub extends below xy plane by distance nub/2 @@ -780,5 +781,244 @@ module snap_pin_socket(size, r, radius, l,length, d,diameter,nub_depth, snap, fi +// Module: rabbit_clip() +// Usage: +// rabbit_clip(type, length, width, snap, thickness, depth, [compression], [clearance], [lock], +// [lock_clearance], [splineteps], [anchor], [orient], [spin]) +// Description: +// Creates a clip with two flexible ears to lock into a mating socket, or create a mask to produce the appropriate +// mating socket. The clip can be made to insert and release easily, or to hold much better, or it can be +// created with locking flanges that will make it very hard or impossible to remove. Unlike the snap pin, this clip +// is rectangular and can be made at any height, so a suitable clip could be very thin. It's also possible to get a +// solid connection with a short pin. +// . +// The type parameters specifies whether to make a clip, a socket mask, or a double clip. The length is the +// total nominal length of the clip. (The actual length will be very close, but not equal to this.) The width +// gives the nominal width of the clip, which is the actual width of the clip at its base. The snap parameter +// gives the depth of the clip sides, which controls how easy the clip is to insert and remove. The clip "ears" are +// made over-wide by the compression value. A nonzero compression helps make the clip secure in its socket. +// The socket's width and length are increased by the clearance value which creates some space and can compensate +// for printing inaccuracy. The socket will be slightly longer than the nominal width. The thickness is the thickness +// curved line that forms the clip. The clip depth is the amount the basic clip shape is extruded. Be sure that you +// make the socket with a larger depth than the clip (try 0.4 mm) to allow ease of insertion of the clip. The clearance +// value does not apply to the depth. The splinesteps parameter increases the sampling of the clip curves. +// . +// By default clips appear with orient=UP and sockets with orient=DOWN. +// . +// The first figure shows the dimensions of the rabbit clip. The second figure shows the clip in red overlayed on +// its socket in yellow. The left clip has a nonzero clearance, so its socket is bigger than the clip all around. +// The right hand locking clip has no clearance, but it has a lock clearance, which provides some space behind +// the lock to allow the clip to fit. (Note that depending on your printer, this can be set to zero.) +// +// Figure(2DMed): +// snap=1.5; +// comp=0.75; +// mid = 8.053; // computed in rabbit_clip +// tip = [-4.58,18.03]; +// translate([9,3]){ +// back_half() +// rabbit_clip("pin",width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap, orient=BACK); +// color("blue"){ +// stroke([[6,0],[6,18]],width=0.1); +// stroke([[6+comp, 12], [6+comp, 18]], width=.1); +// } +// color("red"){ +// stroke([[6-snap,mid], [6,mid]], endcaps="arrow2",width=0.15); +// translate([6+.4,mid-.15])text("snap",size=1,valign="center"); +// translate([6+comp/2,19.5])text("compression", size=1, halign="center"); +// stroke([[6+comp/2,19.3], [6+comp/2,17.7]], endcap2="arrow2", width=.15); +// fwd(1.1)text("width",size=1,halign="center"); +// xflip_copy()stroke([[2,-.7], [6,-.7]], endcap2="arrow2", width=.15); +// move([-6.7,mid])rot(90)text("length", size=1, halign="center"); +// stroke([[-7,10.3], [-7,18]], width=.15, endcap2="arrow2"); +// stroke([[-7,0], [-7,5.8]], width=.15,endcap1="arrow2"); +// stroke([tip, tip-[0,1]], width=.15); +// move([tip.x+2,19.5])text("thickness", halign="center",size=1); +// stroke([[tip.x+2, 19.3], tip+[.1,.1]], width=.15, endcap2="arrow2"); +// } +// } +// +// Figure(2DMed): +// snap=1.5; +// comp=0; +// translate([29,3]){ +// back_half() +// rabbit_clip("socket", width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap, orient=BACK,lock=true); +// color("red")back_half() +// rabbit_clip("pin",width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap, +// orient=BACK,lock=true,lock_clearance=1); +// } +// translate([9,3]){ +// back_half() +// rabbit_clip("socket", clearance=.5,width=12, length=18, depth=1, thickness = 1, +// compression=comp, snap=snap, orient=BACK,lock=false); +// color("red")back_half() +// rabbit_clip("pin",width=12, length=18, depth=1, thickness = 1, compression=comp, snap=snap, +// orient=BACK,lock=false,lock_clearance=1); +// } +// Arguments: +// type = One of "pin", "socket", "male", "female" or "double" to specify what to make. +// length = nominal clip length +// width = nominal clip width +// snap = depth of hollow on the side of the clip +// thickness = thickness of the clip "line" +// depth = amount to extrude clip (give extra room for the socket, about 0.4mm) +// compression = excess width at the "ears" to lock more tightly. Default: 0.1 +// clearance = extra space in the socket for easier insertion. Default: 0.1 +// lock = set to true to make a locking clip that may be irreversible. Default: false +// lock_clearance = give clearance for the lock. Default: 0 +// splinesteps = number of samples in the curves of the clip. Default: 8 +// anchor = anchor point for clip +// orient = clip orientation. Default: UP for pins, DOWN for sockets +// spin = spin the clip. Default: 0 +// +// Example: Here are several sizes that work printed in PLA on a Prusa MK3, with default clearance of 0.1 and a depth of 5 +// module test_pair(length, width, snap, thickness, compression, lock=false) +// { +// depth = 5; +// extra_depth = 10;// Change this to 0.4 for closed sockets +// cuboid([max(width+5,12),12, depth], chamfer=.5, edges=[FRONT,"Y"], anchor=BOTTOM) +// attach(BACK) +// rabbit_clip(type="pin",length=length, width=width,snap=snap,thickness=thickness,depth=depth, +// compression=compression,lock=lock); +// right(width+13) +// diff("remove") +// cuboid([width+8,max(12,length+2),depth+3], chamfer=.5, edges=[FRONT,"Y"], anchor=BOTTOM) +// attach(BACK) +// rabbit_clip(type="socket",length=length, width=width,snap=snap,thickness=thickness,depth=depth+extra_depth, +// lock=lock,compression=0,$tags="remove"); +// } +// left(37)ydistribute(spacing=28){ +// test_pair(length=6, width=7, snap=0.25, thickness=0.8, compression=0.1); +// test_pair(length=3.5, width=7, snap=0.1, thickness=0.8, compression=0.1); // snap = 0.2 gives a firmer connection +// test_pair(length=3.5, width=5, snap=0.1, thickness=0.8, compression=0.1); // hard to take apart +// } +// right(17)ydistribute(spacing=28){ +// test_pair(length=12, width=10, snap=1, thickness=1.2, compression=0.2); +// test_pair(length=8, width=7, snap=0.75, thickness=0.8, compression=0.2, lock=true); // With lock, very firm and irreversible +// test_pair(length=8, width=7, snap=0.75, thickness=0.8, compression=0.2, lock=true); // With lock, very firm and irreversible +// } +// Example: Double clip to connect two sockets +// rabbit_clip("double",length=8, width=7, snap=0.75, thickness=0.8, compression=0.2,depth=5); +// Example: A modified version of the clip that acts like a backpack strap clip, where it locks tightly but you can squeeze to release. +// cuboid([25,15,5],anchor=BOTTOM) +// attach(BACK)rabbit_clip("pin", length=25, width=25, thickness=1.5, snap=2, compression=0, lock=true, depth=5, lock_clearance=3); +// left(32) +// diff("remove") +// cuboid([30,30,11],orient=BACK,anchor=BACK){ +// attach(BACK)rabbit_clip("socket", length=25, width=25, thickness=1.5, snap=2, compression=0, lock=true, depth=5.5, lock_clearance=3,$tags="remove"); +// xflip_copy() +// position(FRONT+LEFT) +// xscale(0.8) +// zcyl(l=20,r=13.5, $tags="remove",$fn=64); +// } +module rabbit_clip(type, length, width, snap, thickness, depth, compression=0.1, clearance=.1, lock=false, lock_clearance=0, + splinesteps=8, anchor, orient, spin=0) +{ + assert(is_num(width) && width>0,"Width must be a positive value"); + assert(is_num(length) && length>0, "Length must be a positive value"); + assert(is_num(thickness) && thickness>0, "Thickness must be a positive value"); + assert(is_num(snap) && snap>=0, "Snap must be a non-negative value"); + assert(is_num(depth) && depth>0, "Depth must be a positive value"); + assert(is_num(compression) && compression >= 0, "Compression must be a nonnegative value"); + assert(is_bool(lock)); + assert(is_num(lock_clearance)); + legal_types = ["pin","socket","male","female","double"]; + assert(in_list(type,legal_types),str("type must be one of ",legal_types)); + + if (type=="double") { + attachable(size=[width+2*compression, depth, 2*length], anchor=default(anchor,BACK), spin=spin, orient=default(orient,BACK)){ + union(){ + rabbit_clip("pin", length=length, width=width, snap=snap, thickness=thickness, depth=depth, compression=compression, + lock=lock, anchor=BOTTOM, orient=UP); + rabbit_clip("pin", length=length, width=width, snap=snap, thickness=thickness, depth=depth, compression=compression, + lock=lock, anchor=BOTTOM, orient=DOWN); + cuboid([width-thickness, depth, thickness]); + } + children(); + } + } else { + anchor = default(anchor,BOTTOM); + is_pin = in_list(type,["pin","male"]); + default_overlap = 0.01 * (is_pin?1:-1); // Shift by this much to undo default overlap + extra = 0.02; // Amount of extension below nominal based position for the socket, must exceed default overlap of 0.01 + clearance = is_pin ? 0 : clearance; + compression = is_pin ? compression : 0; + orient = is_def(orient) ? orient + : is_pin ? UP + : DOWN; + earwidth = 2*thickness+snap; + point_length = earwidth/2.15; + // The adjustment is using cos(theta)*earwidth/2 and sin(theta)*point_length, but the computation + // is obscured because theta is atan(length/2/snap) + scaled_len = length - 0.5 * (earwidth * snap + point_length * length) / sqrt(sqr(snap)+sqr(length/2)); + bottom_pt = [0,max(scaled_len*0.15+thickness, 2*thickness)]; + ctr = [width/2,scaled_len] + line_normal([width/2-snap, scaled_len/2], [width/2, scaled_len]) * earwidth/2; + inside_pt = circle_circle_tangents(bottom_pt, 0, ctr, earwidth/2)[0][1]; + sidepath =[ + [width/2,0], + [width/2-snap,scaled_len/2], + [width/2+(is_pin?compression:0), scaled_len], + ctr - point_length * line_normal([width/2,scaled_len], inside_pt), + inside_pt + ]; + fullpath = concat( + sidepath, + [bottom_pt], + reverse(apply(xflip(),sidepath)) + ); + assert(fullpath[4].y < fullpath[3].y, "Pin is too wide for its length"); + + snapmargin = -snap + select(sidepath,-1).x;// - compression; + if (is_pin){ + if (snapmargin<0) echo("WARNING: The snap is too large for the clip to squeeze to fit its socket") + echo(snapmargin=snapmargin); + } + // Force tangent to be vertical at the outer edge of the clip to avoid overshoot + fulltangent = list_set(path_tangents(fullpath, uniform=false),[2,8], [[0,1],[0,-1]]); + + subset = is_pin ? [0:10] : [0,1,2,3, 7,8,9,10]; // Remove internal points from the socket + tangent = select(fulltangent, subset); + path = select(fullpath, subset); + + socket_smooth = .04; + pin_smooth = [.075, .075, .15, .12, .06]; + smoothing = is_pin + ? concat(pin_smooth, reverse(pin_smooth)) + : let(side_smooth=select(pin_smooth, 0, 2)) + concat(side_smooth, [socket_smooth], reverse(side_smooth)); + bez = path_to_bezier(path,relsize=smoothing,tangents=tangent); + rounded = bezier_polyline(bez,splinesteps=splinesteps); + bounds = pointlist_bounds(rounded); + kk = search([bounds[1].y], subindex(rounded,1)); + echo(rounded[kk[0]]); + extrapt = is_pin ? [] : [rounded[0] - [0,extra]]; + finalpath = is_pin ? rounded + : let(withclearance=offset(rounded, r=-clearance)) + concat( [[withclearance[0].x,-extra]], + withclearance, + [[-withclearance[0].x,-extra]]); + attachable(size=[bounds[1].x-bounds[0].x, depth, bounds[1].y-bounds[0].y], anchor=anchor, spin=spin, orient=orient){ + xrot(90) + translate([0,-(bounds[1].y-bounds[0].y)/2+default_overlap,-depth/2]) + linear_extrude(height=depth, convexity=10) { + if (lock) + xflip_copy() + right(clearance) + polygon([sidepath[1]+[-thickness/10,lock_clearance], + sidepath[2], + [sidepath[2].x,sidepath[1].y+lock_clearance]]); + if (is_pin) + offset_stroke(finalpath, width=[thickness,0]); + else + polygon(finalpath); + } + children(); + } + } +} + + + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap From 7daa16f2382d62e7acd01363a35feb95c6d158c3 Mon Sep 17 00:00:00 2001 From: Garth Minette Date: Mon, 17 Aug 2020 18:11:49 -0700 Subject: [PATCH 5/5] Fixed VNF attachment normal vector. --- attachments.scad | 17 +++++++++++++++-- version.scad | 2 +- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/attachments.scad b/attachments.scad index 2ac4367..c2c969e 100644 --- a/attachments.scad +++ b/attachments.scad @@ -473,7 +473,7 @@ function find_anchor(anchor, geom) = pt = polygon_line_intersection(poly, [cp,cp+anchor], bounded=[true,false], eps=eps) ) if (!is_undef(pt)) let( plane = plane_from_polygon(poly), - n = plane_normal(plane) + n = unit(plane_normal(plane)) ) [norm(pt-cp), n, pt] ] @@ -483,7 +483,20 @@ function find_anchor(anchor, geom) = furthest = max_index(subindex(hits,0)), dist = hits[furthest][0], pos = hits[furthest][2], - n = unit(sum([for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]])) + hitnorms = [for (hit = hits) if (approx(hit[0],dist,eps=eps)) hit[1]], + unorms = len(hitnorms) > 7 + ? unique([for (nn = hitnorms) quant(nn,1e-9)]) + : [ + for (i = idx(hitnorms)) let( + nn = hitnorms[i], + isdup = [ + for (j = [i+1:1:len(hitnorms)-1]) + if (approx(nn, hitnorms[j])) 1 + ] != [] + ) if (!isdup) nn + ], + n = unit(sum(unorms)), + oang = approx(point2d(n), [0,0])? 0 : atan2(n.y, n.x) + 90 ) [anchor, pos, n, oang] ) : type == "vnf_extent"? ( //vnf diff --git a/version.scad b/version.scad index 85cad34..1420f58 100644 --- a/version.scad +++ b/version.scad @@ -8,7 +8,7 @@ ////////////////////////////////////////////////////////////////////// -BOSL_VERSION = [2,0,404]; +BOSL_VERSION = [2,0,405]; // Section: BOSL Library Version Functions