diff --git a/attachments.scad b/attachments.scad index fa8350f..94276d7 100644 --- a/attachments.scad +++ b/attachments.scad @@ -12,10 +12,12 @@ // FileFootnotes: STD=Included in std.scad ////////////////////////////////////////////////////////////////////// +include // Default values for attachment code. $tags=undef; // for backward compatibility $tag = ""; +$save_tag = undef; $tag_prefix = ""; $overlap = 0; $color = "default"; @@ -100,18 +102,18 @@ _ANCHOR_TYPES = ["intersect","hull"]; // stepper motor shape. The names, positions, directions, and spins of these anchors are // specific to the object, and are documented when they exist. // Subsection: Spin -// Spin is specified with the `spin` argument in most shape modules. Specifying a scalar `spin` -// when creating an object will rotate the object counter-clockwise around the Z axis by the given -// number of degrees. If given as a 3D vector, the object will be rotated around each of the X, Y, Z -// axes by the number of degrees in each component of the vector. Spin is always applied after -// anchoring, and before orientation. Since spin is applied after anchoring it is not what -// you might think of intuitively as spinning the shape. To do that, apply `zrot()` to the shape before anchoring. +// Spin is specified with the `spin` argument in most shape modules. Specifying a spin` +// angle when creating an object will rotate the object counter-clockwise around the Z axis by the given +// number of degrees. Spin is always applied after anchoring, and before orientation. +// Since spin is applied after anchoring it is not always what you might think of intuitively +// as spinning the shape. To do that, apply `zrot()` to the shape before anchoring. // Subsection: Orient // Orientation is specified with the `orient` argument in most shape modules. Specifying `orient` // when creating an object will rotate the object such that the top of the object will be pointed // at the vector direction given in the `orient` argument. Orientation is always applied after // anchoring and spin. The constants `UP`, `DOWN`, `FRONT`, `BACK`, `LEFT`, and `RIGHT` can be -// added together to form the directional vector for this. ie: `LEFT+BACK` +// added together to form the directional vector for this (e.g. `LEFT+BACK`). The orient parameter +// is ignored when you use {{attach()}} because {{attach()}} provides its own orientation. // Subsection: Specifying Directions // You can use direction vectors to specify anchors for objects or to specify edges, faces, and // corners of cubes. You can simply specify these direction vectors numerically, but another @@ -772,10 +774,17 @@ function _make_anchor_legal(anchor,geom) = // up on the top while aligning it with the right edge of the top face, and `attach(RIGHT,BOT,align=TOP)` which // stand the object on the right face while aligning with the top edge. If you apply spin using the // argument to `attach()` then it will be taken into account for the alignment. If you apply spin with -// a parameter to the child it will NOT be taken into account. Note that spin is not permitted for +// a parameter to the child it will NOT be taken into account. The special spin value "align" will +// spin the child so that the child's BACK direction is pointed towards the aligned edge on the parent. +// Note that spin is not permitted for // 2D objects because it would change the child orientation so that the anchors are no longer parallel. // When you use `align=` you can also adjust the position using `inset=`, which shifts the child -// away from the edge or corner it is aligned to. +// away from the edge or corner it is aligned to. +// . +// Note that the concept of alignment doesn't always make sense for objects without corners, such as spheres or cylinders. +// In same cases the alignments using such children will be odd because the alignment computation is trying to +// place a non-existent corner somewhere. Because attach() doesn't have in formation about the child when +// it runs it cannot handle curved shapes differently from cubes, so this behavior cannot be changed. // . // If you give `inside=true` then the anchor arrows are lined up so they are pointing the same direction and // the child object will be located inside the parent. In this case a default "remove" tag is applied to @@ -820,7 +829,7 @@ function _make_anchor_legal(anchor,geom) = // overlap = Amount to sink child into the parent. Equivalent to `down(X)` after the attach. This defaults to the value in `$overlap`, which is `0` by default. // inside = If `child` is given you can set `inside=true` to attach the child to the inside of the parent for diff() operations. Default: false // shiftout = Shift an inside object outward so that it overlaps all the aligned faces. Default: 0 -// spin = Amount to rotate the parent around the axis of the parent anchor. (Only permitted in 3D.) +// spin = Amount to rotate the parent around the axis of the parent anchor. Can set to "align" to align the child's BACK with the parent aligned edge. (Only permitted in 3D.) // Side Effects: // `$anchor` set to the parent anchor value used for the child. // `$align` set to the align value used for the child. @@ -828,6 +837,8 @@ function _make_anchor_legal(anchor,geom) = // `$attach_anchor` for each anchor given, this is set to the `[ANCHOR, POSITION, ORIENT, SPIN]` information for that anchor. // if inside is true then set default tag to "remove" // `$attach_to` is set to the value of the `child` argument, if given. Otherwise, `undef` +// `$edge_angle` is set to the angle of the edge if the anchor is on an edge and the parent is a prismoid or vnf with "hull" anchoring +// `$edge_length` is set to the length of the edge if the anchor is on an edge and the parent is a prismoid or vnf with "hull" anchoring // Example: Cylinder placed on top of cube: // cuboid(50) // attach(TOP,BOT) cylinder(d1=30,d2=15,h=25); @@ -878,12 +889,39 @@ function _make_anchor_legal(anchor,geom) = // attach(RIGHT+FRONT, TOP, inside=true) cuboid([10,3,5]); // attach(RIGHT+FRONT, TOP, inside=true, align=TOP,shiftout=.01) cuboid([5,1,2]); // } +// Example: Attaching a 3d edge mask. Simple 2d masks can be done using {{edge_profile()}} but this mask varies along its length. +// module wavy_edge(length,cycles, r, steps, n) +// { +// rmin = is_vector(r) ? r[0] : 0.01; +// rmax = is_vector(r) ? r[1] : r; +// layers = [for(z=[0:steps]) +// let( +// r=rmin+(rmax-rmin)/2*(cos(z*360*cycles/steps)+1),ff=echo(r=r) +// ) +// path3d( concat([[0,0]], +// arc(corner=path2d([BACK,CTR,RIGHT]), n=n, r=r)), +// z/steps*length-length/2) +// ]; +// attachable([rmax,rmax,length]){ +// skin(layers,slices=0); +// children(); +// } +// } +// diff() +// cuboid(25) +// attach([TOP+RIGHT,TOP+LEFT,TOP+FWD, FWD+RIGHT], FWD+LEFT, inside=true, shiftout=.01) +// wavy_edge(length=25.1,cycles=1.4,r=4,steps=24,n=15); + module attach(parent, child, overlap, align, spin=0, norot, inset=0, shiftout=0, inside=false, from, to) { dummy3= assert(num_defined([to,child])<2, "Cannot combine deprecated 'to' argument with 'child' parameter") - assert(num_defined([from,parent])<2, "Cannot combine deprecated 'from' argument with 'parent' parameter"); + assert(num_defined([from,parent])<2, "Cannot combine deprecated 'from' argument with 'parent' parameter") + assert(spin!="align" || is_def(align), "Can only set spin to \"align\" when the 'align' parameter is given") + assert(is_finite(spin) || spin=="align", "Spin must be a number (unless align is given)") + assert((is_undef(overlap) || is_finite(overlap)) && (is_def(overlap) || is_undef($overlap) || is_finite($overlap)), + str("Provided ",is_def(overlap)?"":"$","overlap is not valid.")); if (is_def(to)) echo("The 'to' option to attach() is deprecated and will be removed in the future. Use 'child' instead."); if (is_def(from)) @@ -893,10 +931,16 @@ module attach(parent, child, overlap, align, spin=0, norot, inset=0, shiftout=0, req_children($children); dummy=assert($parent_geom != undef, "No object to attach to!") - assert(is_undef(child) || is_string(child) || (is_vector(child) && (len(child)==2 || len(child)==3)), "child must be a named anchor (a string) or a 2-vector or 3-vector") + assert(is_undef(child) || is_string(child) || (is_vector(child) && (len(child)==2 || len(child)==3)), + "child must be a named anchor (a string) or a 2-vector or 3-vector") assert(is_undef(align) || !is_string(child), "child is a named anchor. Named anchors are not supported with align="); - + two_d = _attach_geom_2d($parent_geom); + basegeom = $parent_geom[0]=="conoid" ? attach_geom(r=2,h=2,axis=$parent_geom[5]) + : $parent_geom[0]=="prismoid" ? attach_geom(size=[2,2,2],axis=$parent_geom[4]) + : attach_geom(size=[2,2,2]); + childgeom = attach_geom([2,2,2]); + child_abstract_anchor = is_vector(child) && !two_d ? _find_anchor(_make_anchor_legal(child,childgeom), childgeom) : undef; overlap = (overlap!=undef)? overlap : $overlap; parent = first_defined([parent,from]); anchors = is_vector(parent) || is_string(parent) ? [parent] : parent; @@ -923,34 +967,63 @@ module attach(parent, child, overlap, align, spin=0, norot, inset=0, shiftout=0, : point3d(anchors[anch_ind]); $anchor=anchor; anchor_data = _find_anchor(anchor, $parent_geom); + $edge_angle = len(anchor_data)==5 ? struct_val(anchor_data[4],"edge_angle") : undef; + $edge_length = len(anchor_data)==5 ? struct_val(anchor_data[4],"edge_length") : undef; anchor_pos = anchor_data[1]; anchor_dir = factor*anchor_data[2]; anchor_spin = two_d || !inside || anchor==TOP || anchor==BOT ? anchor_data[3] : let(spin_dir = rot(anchor_data[3],from=UP, to=-anchor_dir, p=BACK)) _compute_spin(anchor_dir,spin_dir); + parent_abstract_anchor = is_vector(anchor) && !two_d ? _find_anchor(_make_anchor_legal(anchor,basegeom),basegeom) : undef; for(align_ind = idx(align_list)){ align = is_undef(align_list[align_ind]) ? undef : assert(is_vector(align_list[align_ind],2) || is_vector(align_list[align_ind],3), "align direction must be a 2-vector or 3-vector") two_d ? _force_anchor_2d(align_list[align_ind]) : point3d(align_list[align_ind]); + spin = is_num(spin) ? spin + : align==CENTER ? 0 + : sum(v_abs(anchor))==1 ? // parent anchor is a face + let( + spindir = in_list(anchor,[TOP,BOT]) ? BACK : UP, + proj = project_plane(point4d(anchor),[spindir,align]), + ang = v_theta(proj[1])-v_theta(proj[0]) + ) + ang + : // parent anchor is not a face, so must be an edge (corners not allowed) + let( + nativeback = apply(rot(to=parent_abstract_anchor[2],from=UP) + *affine3d_zrot(parent_abstract_anchor[3]), BACK) + ) + nativeback*align<0 ? -180:0; $idx = align_ind+len(align_list)*anch_ind; $align=align; + goodcyl = $parent_geom[0] != "conoid" || is_undef(align) || align==CTR ? true + : let( + align=rot(from=$parent_geom[5],to=UP,p=align), + anchor=rot(from=$parent_geom[5],to=UP,p=anchor) + ) + anchor==TOP || anchor==BOT || align==TOP || align==BOT; + badcorner = !in_list($parent_geom[0],["conoid","spheroid"]) && !is_undef(align) && align!=CTR && sum(v_abs(anchor))==3; + badsphere = $parent_geom[0]=="spheroid" && !is_undef(align) && align!=CTR; dummy=assert(is_undef(align) || all_zero(v_mul(anchor,align)), - str("Invalid alignment: align value (",align,") includes component parallel to parent anchor (",anchor,")")); + str("Invalid alignment: align value (",align,") includes component parallel to parent anchor (",anchor,")")) + assert(goodcyl, str("Cannot use align with an anchor on a curved edge or surface of a cylinder at parent anchor (",anchor,")")) + assert(!badcorner, str("Cannot use align at a corner anchor (",anchor,")")) + assert(!badsphere, "Cannot use align on spheres."); // Now compute position on the parent (including alignment but not inset) where the child will be anchored pos = is_undef(align) ? anchor_data[1] : _find_anchor(anchor+align, $parent_geom)[1]; $attach_anchor = list_set(anchor_data, 1, pos); // Never used; For user informational use? Should this be set at all? - startdir = two_d || is_undef(align)? undef - : anchor==UP || anchor==DOWN ? BACK - : UP - (anchor*UP)*anchor/(anchor*anchor); // Component of UP perpendicular to anchor - enddir = is_undef(child) || child.z==0 ? UP : BACK; // Compute adjustment to the child anchor for position purposes. This adjustment - // accounts for the change in the anchor needed to to alignment. + // accounts for the change in the anchor needed to to alignment. child_adjustment = is_undef(align)? CTR : two_d ? rot(to=child,from=-factor*anchor,p=align) - : apply( frame_map(x=child, z=enddir) - *frame_map(x=-factor*anchor, z=startdir, reverse=true) - *rot(v=anchor,-spin), align); + : apply( rot(to=child_abstract_anchor[2],from=UP) + * affine3d_zrot(child_abstract_anchor[3]) + * affine3d_yrot(inside?0:180) + * affine3d_zrot(-parent_abstract_anchor[3]) + * rot(from=parent_abstract_anchor[2],to=UP) + * rot(v=anchor,-spin), + align); // The $anchor_override anchor value forces an override of the *position* only for the anchor // used when attachable() places the child $anchor_override = all_zero(child_adjustment)? inside?child:undef @@ -959,8 +1032,11 @@ module attach(parent, child, overlap, align, spin=0, norot, inset=0, shiftout=0, // inset_dir is the direction for insetting when alignment is in effect inset_dir = is_undef(align) ? CTR : two_d ? rot(to=reference, from=anchor,p=align) - : apply(zrot(-factor*spin)*frame_map(x=reference, z=BACK)*frame_map(x=factor*anchor, z=startdir, reverse=true), - align); + : apply(affine3d_yrot(inside?180:0) + * affine3d_zrot(-parent_abstract_anchor[3]) + * rot(from=parent_abstract_anchor[2],to=UP) + * rot(v=anchor,-spin), + align); spinaxis = two_d? UP : anchor_dir; olap = - overlap * reference - inset*inset_dir + shiftout * (inset_dir + factor*reference); if (norot || (approx(anchor_dir,reference) && anchor_spin==0)) @@ -981,7 +1057,7 @@ module attach(parent, child, overlap, align, spin=0, norot, inset=0, shiftout=0, // Module: tag() // Synopsis: Assigns a tag to an object // Topics: Attachments -// See Also: force_tag(), recolor(), hide(), show_only(), diff(), intersect() +// See Also: tag_this(), force_tag(), recolor(), hide(), show_only(), diff(), intersect() // Usage: // PARENT() tag(tag) CHILDREN; // Description: @@ -1016,6 +1092,40 @@ module tag(tag) } + +// Module: tag_this() +// Synopsis: Assigns a tag to an object at the current level only. +// Topics: Attachments +// See Also: tag(), force_tag(), recolor(), hide(), show_only(), diff(), intersect() +// Usage: +// PARENT() tag(tag) CHILDREN; +// Description: +// Assigns the specified tag to the children at the current level only, with tags reverting to +// the previous tag in force for deeper descendents. This works using `$tag` and `$save_tag`. +// . +// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments). +// Arguments: +// tag = tag string, which must not contain any spaces. +// Side Effects: +// Sets `$tag` to the tag you specify, possibly with a scope prefix, and saves current tag in `$save_tag`. +// Example(3D): Here we subtract a cube while keeping its child. With {{tag()}} the child would inherit the "remove" tag and we would need to explicitly retag the child to prevent it from also being subtracted. +// diff() +// cuboid([10,10,4]) +// tag_this("remove")position(TOP) cuboid(3) // This cube is subtracted +// attach(TOP,BOT) cuboid(1); // Tag is reset so this cube displays + +module tag_this(tag) +{ + req_children($children); + check= + assert(is_string(tag),"tag must be a string") + assert(undef==str_find(tag," "),str("Tag string \"",tag,"\" contains a space, which is not allowed")); + $save_tag=default($tag,""); + $tag = str($tag_prefix,tag); + children(); +} + + // Module: force_tag() // Synopsis: Assigns a tag to a non-attachable object. // Topics: Attachments @@ -1683,6 +1793,40 @@ module hide(tags) } +// Module: hide_this() +// Synopsis: Hides attachable children at the current level +// Topics: Attachments +// See Also: hide(), tag_this(), tag(), recolor(), show_only(), show_all(), show_int(), diff(), intersect() +// Usage: +// hide_this() CHILDREN; +// Description: +// Hides all attachable children at the current level, while still displaying descendants. +// For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments). +// Side Effects: +// Sets `$tag` and `$save_tag` +// Example: Use an invisible parent to position children. Unlike with {{hide()}} we do not need to explicitly use any tags. +// $fn=16; +// hide_this() cuboid(10) +// { +// attach(RIGHT,BOT) cyl(r=1,h=5); +// attach(LEFT,BOT) cyl(r=1,h=5); +// } +// Example: Nexting applications of hide_this() +// $fn=32; +// hide_this() cuboid(10) +// attach(TOP,BOT) cyl(r=2,h=5) +// hide_this() attach(TOP,BOT) cuboid(4) +// attach(RIGHT,BOT) cyl(r=1,h=2); + +module hide_this() +{ + tag_scope() + hide("child") + tag_this("child") + children(); +} + + // Module: show_only() // Synopsis: Show only the children with the listed tags. // See Also: tag(), recolor(), show_all(), show_int(), diff(), intersect() @@ -1690,7 +1834,7 @@ module hide(tags) // Usage: // show_only(tags) CHILDREN; // Description: -// Show only the children with the listed tags, which you sply as a space separated string. Only unhidden objects will be shown, so if an object is hidden either before or after the `show_only()` call then it will remain hidden. This overrides any previous `show_only()` calls. Unlike `hide()`, calls to `show_only()` are not cumulative. +// Show only the children with the listed tags, which you supply as a space separated string. Only unhidden objects will be shown, so if an object is hidden either before or after the `show_only()` call then it will remain hidden. This overrides any previous `show_only()` calls. Unlike `hide()`, calls to `show_only()` are not cumulative. // For a step-by-step explanation of attachments, see the [Attachments Tutorial](Tutorial-Attachments). // Side Effects: // Sets `$tags_shown` to the tag you specify. @@ -1981,18 +2125,18 @@ module face_profile(faces=[], r, d, excess=0.01, convexity=10) { // cube([50,60,70],center=true) // edge_profile([TOP,"Z"],except=[BACK,TOP+LEFT]) // mask2d_roundover(r=10, inset=2); -// Example: Using $edge_angle on a Conoid +// Example: Using $edge_angle on a conoid // diff() // cyl(d1=50, d2=30, l=40, anchor=BOT) { // edge_profile([TOP,BOT], excess=10, convexity=6) { // mask2d_roundover(r=8, inset=1, excess=1, mask_angle=$edge_angle); // } // } -// Example: Using $edge_angle on a Prismoid +// Example: Using $edge_angle on a prismoid // diff() // prismoid([60,50],[30,20],h=40,shift=[-25,15]) { // edge_profile(excess=10, convexity=20) { -// mask2d_roundover(r=5,inset=1,mask_angle=$edge_angle); +// mask2d_roundover(r=5,inset=1,mask_angle=$edge_angle,$fn=32); // } // } @@ -2864,11 +3008,23 @@ module attachable( children(0); } } - if (is_def($save_color)) { + if (is_def($save_tag) && is_def($save_color)){ + $tag=$save_tag; + $save_tag=undef; $color=$save_color; // Revert to the color before color_this() call $save_color=undef; children(1); } + else if (is_def($save_color)) { + $color=$save_color; // Revert to the color before color_this() call + $save_color=undef; + children(1); + } + else if (is_def($save_tag)) { + $tag=$save_tag; + $save_tag=undef; + children(1); + } else children(1); } } @@ -3482,13 +3638,8 @@ function _attach_transform(anchor, spin, orient, geom, p) = * rot(to=FWD, from=point3d(anch[2])) * affine3d_translate(point3d(-pos)) : - let( - spinT = is_num(spin) ? affine3d_zrot(-anch[3]-spin) - : affine3d_zrot(-spin.z) * affine3d_yrot(-spin.y) * affine3d_xrot(-spin.x) - * affine3d_zrot(-anch[3]) - ) affine3d_yrot(180) - * spinT + * affine3d_zrot(-anch[3]-spin) * rot(from=anch[2],to=UP) * affine3d_translate(point3d(-pos)) ) @@ -3568,7 +3719,7 @@ function _get_cp(geom) = /// Arguments: /// anchor = Vector or named anchor string. /// geom = The geometry description of the shape. -function _find_anchor(anchor, geom) = +function _find_anchor(anchor, geom)= is_string(anchor)? ( anchor=="origin"? [anchor, CENTER, UP, 0] // Ok that this returns 3d anchor in the 2d case? : let( @@ -3596,8 +3747,10 @@ function _find_anchor(anchor, geom) = let(all_comps_good = [for (c=anchor) if (c!=sign(c)) 1]==[]) assert(all_comps_good, "All components of an anchor for a cuboid/prismoid must be -1, 0, or 1") let( - size=geom[1], size2=geom[2], - shift=point2d(geom[3]), axis=point3d(geom[4]), + size=geom[1], + size2=geom[2], + shift=point2d(geom[3]), + axis=point3d(geom[4]), override = geom[5](anchor) ) let( @@ -3610,7 +3763,7 @@ function _find_anchor(anchor, geom) = axy = point2d(anch), bot = point3d(v_mul(point2d(size )/2, axy), -h/2), top = point3d(v_mul(point2d(size2)/2, axy) + shift, h/2), - edge = top-bot, + edge = top-bot, pos = point3d(cp) + lerp(bot,top,u) + offset, // Find vectors of the faces involved in the anchor facevecs = @@ -3619,7 +3772,7 @@ function _find_anchor(anchor, geom) = if (anch.y!=0) unit(rot(from=UP, to=[0,edge.y,max(0.01,h)], p=[0,axy.y,0]), UP), if (anch.z!=0) unit([0,0,anch.z],UP) ], - dir = anchor==CENTER? UP + dir = anch==CENTER? UP : len(facevecs)==1? unit(facevecs[0],UP) : len(facevecs)==2? vector_bisect(facevecs[0],facevecs[1]) : let( @@ -3631,39 +3784,67 @@ function _find_anchor(anchor, geom) = v3 = unit(line[1]-line[0],UP) * anch.z ) unit(v3,UP), - final_dir = default(override[1],rot(from=UP, to=axis, p=dir)), + edgeang = len(facevecs)==2 ? 180-vector_angle(facevecs[0], facevecs[1]) : undef, + final_dir = default(override[1],anch==CENTER?UP:rot(from=UP, to=axis, p=dir)), final_pos = default(override[0],rot(from=UP, to=axis, p=pos)), + // If the anchor is on a face or horizontal edge we take the oang value for spin // If the anchor is on a vertical or sloped edge or corner we want to align the spin to point upward along the edge - // The "native" spin direction is the rotation of UP to the anchor direction - // The desired spin direction is the edge vector - // The axis of rotation is the direction vector, so we need component of edge perpendicular to dir - spin = anchor.x!=0 && anchor.y!=0 ? _compute_spin(dir, edge) //sign(anchor.x)*vector_angle(edge - (edge*dir)*dir/(dir*dir), rot(from=UP,to=dir,p=BACK)) - : oang - ) [anchor, final_pos, final_dir, default(override[2],spin)] + + // Set "vertical" edge and corner anchors point along the edge + spin = anch.x!=0 && anch.y!=0 ? _compute_spin(final_dir, rot(from=UP, to=axis, p=edge)) + // Horizontal anchors point clockwise + : anch.z!=0 && sum(v_abs(anch))==2 ? _compute_spin(final_dir, rot(from=UP, to=axis, p=anch.z*[anch.y,-anch.x,0])) + : norm(anch)==3 ? _compute_spin(final_dir, final_dir==DOWN || final_dir==UP ? BACK : UP) + : oang // face anchors point UP/BACK + ) [anchor, final_pos, final_dir, default(override[2],spin), if (is_def(edgeang)) [["edge_angle",edgeang],["edge_length",norm(edge)]]] ) : type == "conoid"? ( //r1, r2, l, shift - assert(anchor.z == sign(anchor.z), "The Z component of an anchor for a cylinder/cone must be -1, 0, or 1") let( - rr1=geom[1], rr2=geom[2], l=geom[3], - shift=point2d(geom[4]), axis=point3d(geom[5]), + rr1=geom[1], + rr2=geom[2], + length=geom[3], + shift=point2d(geom[4]), + axis=point3d(geom[5]), r1 = is_num(rr1)? [rr1,rr1] : point2d(rr1), r2 = is_num(rr2)? [rr2,rr2] : point2d(rr2), anch = rot(from=axis, to=UP, p=anchor), + axisname = axis==UP ? "Z" + : axis==RIGHT ? "X" + : axis==BACK ? "Y" + : "", + dummy = assert(anch.z == sign(anch.z), str("The ",axisname," component of an anchor for the cylinder/cone must be -1, 0, or 1")), offset = rot(from=axis, to=UP, p=offset), u = (anch.z+1)/2, + // Returns [point,tangent_dir] + solve_ellipse = function (r,dir) approx(dir,[0,0]) ? [[0,0],[0,0]] + : let( + x = r.x*dir.x*r.y / sqrt(dir.x^2*r.y^2+dir.y^2*r.x^2), + y = r.x*dir.y*r.y / sqrt(dir.x^2*r.y^2+dir.y^2*r.x^2) + ) + [[x,y], unit([y*r.x^2,-x*r.y^2],CTR)], + on_center = approx(point2d(anch), [0,0]), + botdata = solve_ellipse(r1,point2d(anch)), + topdata = solve_ellipse(r2,point2d(anch)), + bot = point3d(botdata[0], -length/2), + top = point3d(topdata[0], length/2), + tangent = lerp(botdata[1],topdata[1],u), + normal = [-tangent.y,tangent.x], axy = unit(point2d(anch),[0,0]), - bot = point3d(v_mul(r1,axy), -l/2), - top = point3d(v_mul(r2,axy)+shift, l/2), + obot = point3d(v_mul(r1,axy), -length/2), + otop = point3d(v_mul(r2,axy)+shift, length/2), pos = point3d(cp) + lerp(bot,top,u) + offset, - sidevec = rot(from=UP, to=top==bot?UP:top-bot, p=point3d(axy)), + sidevec = rot(from=UP, to=top==bot?UP:top-bot, p=point3d(normal)), vvec = anch==CENTER? UP : unit([0,0,anch.z],UP), - vec = anch==CENTER? CENTER : - approx(axy,[0,0])? unit(anch,UP) : - approx(anch.z,0)? sidevec : - unit((sidevec+vvec)/2,UP), + vec = on_center? unit(anch,UP) + : approx(anch.z,0)? sidevec + : unit((sidevec+vvec)/2,UP), pos2 = rot(from=UP, to=axis, p=pos), - vec2 = anch==CENTER? UP : rot(from=UP, to=axis, p=vec) - ) [anchor, pos2, vec2, oang] + vec2 = anch==CENTER? UP : rot(from=UP, to=axis, p=vec), + // Set spin for top/bottom to be clockwise + spin = anch.z!=0 && (anch.x!=0 || anch.y!=0) ? _compute_spin(vec2,rot(from=UP,to=axis,p=point3d(tangent)*anch.z)) + : anch.z==0 && norm(anch)>0 ? _compute_spin(vec2, (vec2==DOWN || vec2==UP)?BACK:UP) + : oang + ) [anchor, pos2, vec2, spin] ) : type == "point"? ( let( anchor = unit(point3d(anchor),CENTER), @@ -3742,24 +3923,60 @@ function _find_anchor(anchor, geom) = rpts = apply(rot(from=anchor, to=RIGHT) * move(point3d(-cp)), vnf[0]), maxx = max(column(rpts,0)), idxs = [for (i = idx(rpts)) if (approx(rpts[i].x, maxx)) i], - dir = len(idxs)>2 ? [anchor,oang] - : len(idxs)==2 ? - let( - edgefaces = _vnf_find_edge_faces(vnf,idxs), - edge = select(vnf[0],idxs) + // We want to catch the case where the points lie on an edge. The complication is that the edge + // may appear twice WITH DIFFERENT VERTEX INDICES if repeated points appear in the vnf. + edges_faces = len(idxs)==2 ? // Simple case, no repeated points, [idxs] gives the edge + approx(vnf[0][idxs[0]],vnf[0][idxs[1]]) ? [] // Are edge points identical? + : let( facelist = _vnf_find_edge_faces(vnf,idxs)) + len(facelist)==2 ? [[idxs], facelist] : [] + : len(idxs)!=4 ? [] // If we don't have four points it's not an edge pair + : let( + pts = select(vnf[0],idxs), + matchind = [for(i=[1:3]) if (approx(pts[i],pts[0])) i] // indices where actual vertex point is the same as point zero + ) + len(matchind)!=1 ? [] + : let( // After this runs we have two edges as index pairs, and their associated faces as index values + match1 = select(idxs,[0,matchind[0]]), + match2 = list_remove_values(idxs,match1), + face1 = _vnf_find_edge_faces(vnf,[match1[0],match2[0]]), + face2 = _vnf_find_edge_faces(vnf,[match1[0],match2[1]]), + edge1 = [match1[0], face1==[] ? match2[1] : match2[0]], + edge2 = list_remove_values(idxs,edge1), + face3 = _vnf_find_edge_faces(vnf,edge2), + allfaces = concat(face1,face2,face3) + ) + assert(len(allfaces)==2, "Invalid polyhedron encountered while computing VNF anchor") + [[edge1,edge2], allfaces], + dir = len(idxs)>2 && edges_faces==[] ? [anchor,oang] + : edges_faces!=[] ? + let( + faces = edges_faces[1], + edge = select(vnf[0],edges_faces[0][0]), + facenormals = [for(face=faces) polygon_normal(select(vnf[0],vnf[1][face]))], + direction= unit(mean(facenormals)), + projnormals = project_plane(point4d(cross(facenormals[0],facenormals[1])), facenormals), + ang = 180- posmod(v_theta(projnormals[1])-v_theta(projnormals[0]),360), + horiz_face = [for(i=[0:1]) if (approx(v_abs(facenormals[i]),UP)) i], + spin = horiz_face==[] ? + let( + edgedir = edge[1]-edge[0], + nz = [for(i=[0:2]) if (!approx(edgedir[i],0)) i], + flip = edgedir[last(nz)] < 0 ? -1 : 1 + ) + _compute_spin(direction, flip*edgedir) + : + let( + hedge = len(edges_faces[0])==1 ? edges_faces[0][0] + : edges_faces[0][horiz_face[0]], + face = select(vnf[1],faces[horiz_face[0]]), + edgeind = search([hedge[0]], face)[0], + flip = select(face,edgeind+1)== hedge[1] ? 1 : -1, + edgedir = edge[1]-edge[0] + ) + _compute_spin(direction, flip*edgedir) ) - len(edgefaces)==0 ? [anchor,oang] - : assert(len(edgefaces)==2, "Invalid polyhedron encountered while computing VNF anchor") - edge[0]==edge[1] ? [anchor,oang] // two "edge" points are the same, so give up - : let( - direction= unit(mean([for(face=edgefaces) polygon_normal(select(vnf[0],vnf[1][face]))])), - edgedir = edge[1]-edge[0], - nz = [for(i=[0:2]) if (!approx(edgedir[i],0)) i], - flip = edgedir[last(nz)] < 0 ? -1 : 1, - spin = _compute_spin(direction, flip*edgedir) - ) - [direction,spin] - : let( + [direction,spin,[["edge_angle",ang],["edge_length",norm(edge[0]-edge[1])]]] + : let( // This section handles corner anchors, currently spins just point up vertices = vnf[0], faces = vnf[1], cornerfaces = _vnf_find_corner_faces(vnf,idxs[0]), // faces = [3,9,12] indicating which faces @@ -3777,7 +3994,7 @@ function _find_anchor(anchor, geom) = avep = sum(select(rpts,idxs))/len(idxs), mpt = approx(point2d(anchor),[0,0])? [maxx,0,0] : avep, pos = point3d(cp) + rot(from=RIGHT, to=anchor, p=mpt) - ) [anchor, default(override[0],pos),default(override[1],dir[0]),default(override[2],dir[1])] + ) [anchor, default(override[0],pos),default(override[1],dir[0]),default(override[2],dir[1]),if (len(dir)==3) dir[2]] ) : type == "trapezoid"? ( //size, size2, shift, override let(all_comps_good = [for (c=anchor) if (c!=sign(c)) 1]==[]) assert(all_comps_good, "All components of an anchor for a rectangle/trapezoid must be -1, 0, or 1") @@ -4636,10 +4853,20 @@ function _compute_spin(anchor_dir, spin_dir) = let( native_dir = rot(from=UP, to=anchor_dir, p=BACK), spin_dir = spin_dir - (spin_dir*anchor_dir)*anchor_dir, // component of spin_dir perpendicular to anchor_dir + dummy = assert(!approx(spin_dir,[0,0,0]),"spin direction is parallel to anchor"), angle = vector_angle(native_dir,spin_dir), sign = cross(native_dir,spin_dir)*anchor_dir<0 ? -1 : 1 ) sign*angle; + +// Compute canonical edge direction so that edge is either Z+, Y+ or X+ in that order +function _canonical_edge(edge) = + let( + nz = [for(i=[0:2]) if (!approx(edge[i],0)) i], + flip = edge[last(nz)] < 0 ? -1 : 1 + ) + flip * edge; + // vim: expandtab tabstop=4 shiftwidth=4 softtabstop=4 nowrap diff --git a/drawing.scad b/drawing.scad index ed63276..47fc168 100644 --- a/drawing.scad +++ b/drawing.scad @@ -786,7 +786,7 @@ function arc(n, r, angle, d, cp, points, corner, width, thickness, start, wedge= plane = [corner[2], corner[0], corner[1]], points2d = project_plane(plane, corner) ) - lift_plane(plane,arc(n,corner=points2d,wedge=wedge,long=long)) + lift_plane(plane,arc(n,corner=points2d,wedge=wedge,r=r, d=d)) ) : assert(is_path(corner) && len(corner) == 3) let(col = is_collinear(corner[0],corner[1],corner[2])) diff --git a/tutorials/Attachments.md b/tutorials/Attachments.md index 9e8c2db..6e9592b 100644 --- a/tutorials/Attachments.md +++ b/tutorials/Attachments.md @@ -223,15 +223,6 @@ include cube([20,20,40], center=true, spin=45); ``` -You can also spin around other axes, or multiple axes at once, by giving 3 angles (in degrees) to -`spin=` as a vector, like [Xang,Yang,Zang]. Similarly to `rotate()`, -the rotations apply in the order given, X-axis spin, then Y-axis, then Z-axis: - -```openscad-3D -include -cube([20,20,40], center=true, spin=[10,20,30]); -``` - This example shows a cylinder which has been anchored at its FRONT, with a rotated copy in gray. The rotation is performed around the origin, but the cylinder is off the origin, so the rotation **does** @@ -666,13 +657,13 @@ To show all the standard cardinal anchor points, you can use the [show_anchors() ```openscad-3D;Big include -cube(40, center=true) +cube(20, center=true) show_anchors(); ``` ```openscad-3D;Big include -cylinder(h=40, d=40, center=true) +cylinder(h=25, d=25, center=true) show_anchors(); ``` @@ -687,7 +678,56 @@ For large objects, you can again change the size of the arrows with the `s=` arg ```openscad-3D;Big include prismoid(150,60,100) - show_anchors(s=35); + show_anchors(s=45); +``` + +Is is also possible to attach to edges and corners of the parent +object. The anchors for edges spin the child so its BACK direction is +aligned with the edge. If the edge belongs to a top or bottom +horizontal face, then the BACK directions will point clockwise around +the face, as seen from outside the shape. (This is the same direction +required for construction of valid faces in OpenSCAD.) Otherwise, the +BACK direction will point upwards. + +Examine the red flags below, where only edge anchors appear on a +prismoid. The top face shows the red flags pointing clockwise. +The sloped side edges point along the edges, generally upward, and +the bottom ones appear to point counter-clockwise, but if we viewed +the shape from the bottom they would also appear clockwise. + +```openscad-3D;Big +include +prismoid([100,175],[55,88], h=55) + for(i=[-1:1], j=[-1:1], k=[-1:1]) + let(anchor=[i,j,k]) + if (sum(v_abs(anchor))==2) + attach(anchor,BOT)anchor_arrow(40); +``` + +In this example cylinders sink half-way into the top edges of the +prismoid: + +```openscad-3D;Big +include +$fn=16; +r=6; +prismoid([100,175],[55,88], h=55){ + attach([TOP+RIGHT,TOP+LEFT],LEFT,overlap=r/2) cyl(r=r,l=88+2*r,rounding=r); + attach([TOP+FWD,TOP+BACK],LEFT,overlap=r/2) cyl(r=r,l=55+2*r, rounding=r); +} +``` + +This type of edge attachment is useful for attaching 3d edge masks to +edges: + +```openscad-3D;Big +include +$fn=32; +diff() +cuboid(75) + attach([FRONT+LEFT, FRONT+RIGHT, BACK+LEFT, BACK+RIGHT], + FWD+LEFT,inside=true) + rounding_edge_mask(l=76, r1=8,r2=28); ``` ## Parent-Child Anchor Attachment (Double Argument Attachment) @@ -2019,7 +2059,8 @@ override the position. If you omit the other list items then the value drived from the standard anchor will be used. Below we override position of the FWD anchor: -``` +```openscad-3D +include module cubic_barbell(s=100, anchor=CENTER, spin=0, orient=UP) { override = [ [FWD, [[0,-s/8,0]]] @@ -2039,7 +2080,8 @@ Note how the FWD anchor is now rooted on the cylindrical portion. If you wanted to also change its direction and spin you could do it like this: -``` +```openscad-3D +include module cubic_barbell(s=100, anchor=CENTER, spin=0, orient=UP) { override = [ [FWD, [[0,-s/8,0], FWD+LEFT, 225]] @@ -2062,7 +2104,9 @@ The third entry gives a spin override, whose effect is shown by the position of the red flag on the arrow. If you want to override all of the x=0 anchors to be on the cylinder, with their standard directions, you can do that by supplying a list: -``` + +```openscad-3D +include module cubic_barbell(s=100, anchor=CENTER, spin=0, orient=UP) { override = [ for(j=[-1:1:1], k=[-1:1:1]) @@ -2086,7 +2130,8 @@ the default, or a `[position, direction, spin]` triple to override the default. As before, you can omit values to keep their default. Here is the same example using a function literal for the override: -``` +```openscad-3D +include module cubic_barbell(s=100, anchor=CENTER, spin=0, orient=UP) { override = function (anchor) anchor.x!=0 || anchor==CTR ? undef // Keep these