Skip to content

Tutorial Attachment Making

Adrian Mariano edited this page Jun 3, 2025 · 1 revision

Making Attachables

To make a shape attachable, you just need to wrap it with an attachable() module with a basic description of the shape's geometry. By default, the shape is expected to be centered at the origin. This fact is important, as this is how the attachable() module knows the dimensions of your part: it assumes it is symetric around the origin. The attachable() module expects exactly two children. The first will bethe shape to make attachable, and the second will be children(), literally.

Pass-through Attachables

The simplest way to make your own attachable module is to simply pass through to a pre-existing attachable submodule. This could be appropriate if you want to rename a module, or if the anchors of an existing module are suited to (or good enough for) your object. In order for your attachable module to work properly you need to accept the anchor, spin and orient parameters, give them suitable defaults, and pass them to the attachable submodule. Don't forget to pass the children to the attachable submodule as well, or your new module will ignore its children.

include <BOSL2/std.scad>
$fn=32;
module cutcube(anchor=CENTER,spin=0,orient=UP)
{
   tag_scope(){
     diff()
       cuboid(15, rounding=2, anchor=anchor,spin=spin,orient=orient){
         tag("remove")attach(TOP)cuboid(5);
         children();
       }
   }
}
diff()
cutcube()
  tag("remove")attach(RIGHT) cyl(d=2,h=8);

Figure 1

Prismoidal/Cuboidal Attachables

To make a cuboidal or prismoidal shape attachable, you use the size, size2, and offset arguments of attachable().

In the most basic form, where the shape is fully cuboid, with top and bottom of the same size, and directly over one another, you can just use size=.

include <BOSL2/std.scad>
module cubic_barbell(s=100, anchor=CENTER, spin=0, orient=UP) {
    attachable(anchor,spin,orient, size=[s*3,s,s]) {
        union() {
            xcopies(2*s) cube(s, center=true);
            xcyl(h=2*s, d=s/4);
        }
        children();
    }
}
cubic_barbell(100) show_anchors(60);

Figure 2

When the shape is prismoidal, where the top is a different size from the bottom, you can use the size2= argument as well. While size= takes all three axes sizes, the size2= argument only takes the [X,Y] sizes of the top of the shape.

include <BOSL2/std.scad>
module prismoidal(size=[100,100,100], scale=0.5, anchor=CENTER, spin=0, orient=UP) {
    attachable(anchor,spin,orient, size=size, size2=[size.x, size.y]*scale) {
        hull() {
            up(size.z/2-0.005)
                linear_extrude(height=0.01, center=true)
                    square([size.x,size.y]*scale, center=true);
            down(size.z/2-0.005)
                linear_extrude(height=0.01, center=true)
                    square([size.x,size.y], center=true);
        }
        children();
    }
}
prismoidal([100,60,30], scale=0.5) show_anchors(20);

Figure 3

When the top of the prismoid can be shifted away from directly above the bottom, you can use the shift= argument. The shift= argument takes an [X,Y] vector of the offset of the center of the top from the XY center of the bottom of the shape.

include <BOSL2/std.scad>
module prismoidal(size=[100,100,100], scale=0.5, shift=[0,0], anchor=CENTER, spin=0, orient=UP) {
    attachable(anchor,spin,orient, size=size, size2=[size.x, size.y]*scale, shift=shift) {
        hull() {
            translate([shift.x, shift.y, size.z/2-0.005])
                linear_extrude(height=0.01, center=true)
                    square([size.x,size.y]*scale, center=true);
            down(size.z/2-0.005)
                linear_extrude(height=0.01, center=true)
                    square([size.x,size.y], center=true);
        }
        children();
    }
}
prismoidal([100,60,30], scale=0.5, shift=[-30,20]) show_anchors(20);

Figure 4

In the case that the prismoid is not oriented vertically, (ie, where the shift= or size2= arguments should refer to a plane other than XY) you can use the axis= argument. This lets you make prismoids naturally oriented forwards/backwards or sideways.

include <BOSL2/std.scad>
module yprismoidal(
    size=[100,100,100], scale=0.5, shift=[0,0],
    anchor=CENTER, spin=0, orient=UP
) {
    attachable(
        anchor, spin, orient,
        size=size, size2=point2d(size)*scale,
        shift=shift, axis=BACK
    ) {
        xrot(-90) hull() {
            translate([shift.x, shift.y, size.z/2-0.005])
                linear_extrude(height=0.01, center=true)
                    square([size.x,size.y]*scale, center=true);
            down(size.z/2-0.005)
                linear_extrude(height=0.01, center=true)
                    square([size.x,size.y], center=true);
        }
        children();
    }
}
yprismoidal([100,60,30], scale=1.5, shift=[20,20]) show_anchors(20);

Figure 5

Cylindrical Attachables

To make a cylindrical shape attachable, you use the l, and r/d, args of attachable().

include <BOSL2/std.scad>
module twistar(l,r,d, anchor=CENTER, spin=0, orient=UP) {
    r = get_radius(r=r,d=d,dflt=1);
    attachable(anchor,spin,orient, r=r, l=l) {
        linear_extrude(height=l, twist=90, slices=20, center=true, convexity=4)
            star(n=20, r=r, ir=r*0.9);
        children();
    }
}
twistar(l=100, r=40) show_anchors(20);

Figure 6

If the cylinder is elipsoidal in shape, you can pass the unequal X/Y sizes as a 2-item vector to the r= or d= argument.

include <BOSL2/std.scad>
module ovalstar(l,rx,ry, anchor=CENTER, spin=0, orient=UP) {
    attachable(anchor,spin,orient, r=[rx,ry], l=l) {
        linear_extrude(height=l, center=true, convexity=4)
            scale([1,ry/rx,1])
                star(n=20, r=rx, ir=rx*0.9);
        children();
    }
}
ovalstar(l=100, rx=50, ry=30) show_anchors(20);

Figure 7

For cylindrical shapes that aren't oriented vertically, use the axis= argument.

include <BOSL2/std.scad>
module ytwistar(l,r,d, anchor=CENTER, spin=0, orient=UP) {
    r = get_radius(r=r,d=d,dflt=1);
    attachable(anchor,spin,orient, r=r, l=l, axis=BACK) {
        xrot(-90)
            linear_extrude(height=l, twist=90, slices=20, center=true, convexity=4)
                star(n=20, r=r, ir=r*0.9);
        children();
    }
}
ytwistar(l=100, r=40) show_anchors(20);

Figure 8

Conical Attachables

To make a conical shape attachable, you use the l, r1/d1, and r2/d2, args of attachable().

include <BOSL2/std.scad>
module twistar(l, r,r1,r2, d,d1,d2, anchor=CENTER, spin=0, orient=UP) {
    r1 = get_radius(r1=r1,r=r,d1=d1,d=d,dflt=1);
    r2 = get_radius(r1=r2,r=r,d1=d2,d=d,dflt=1);
    attachable(anchor,spin,orient, r1=r1, r2=r2, l=l) {
        linear_extrude(height=l, twist=90, scale=r2/r1, slices=20, center=true, convexity=4)
            star(n=20, r=r1, ir=r1*0.9);
        children();
    }
}
twistar(l=100, r1=40, r2=20) show_anchors(20);

Figure 9

If the cone is ellipsoidal in shape, you can pass the unequal X/Y sizes as a 2-item vectors to the r1=/r2= or d1=/d2= arguments.

include <BOSL2/std.scad>
module ovalish(l,rx1,ry1,rx2,ry2, anchor=CENTER, spin=0, orient=UP) {
    attachable(anchor,spin,orient, r1=[rx1,ry1], r2=[rx2,ry2], l=l) {
        hull() {
            up(l/2-0.005)
                linear_extrude(height=0.01, center=true)
                    ellipse([rx2,ry2]);
            down(l/2-0.005)
                linear_extrude(height=0.01, center=true)
                    ellipse([rx1,ry1]);
        }
        children();
    }
}
ovalish(l=100, rx1=50, ry1=30, rx2=30, ry2=50) show_anchors(20);

Figure 10

For conical shapes that are not oriented vertically, use the axis= argument to indicate the direction of the primary shape axis:

include <BOSL2/std.scad>
module ytwistar(l, r,r1,r2, d,d1,d2, anchor=CENTER, spin=0, orient=UP) {
    r1 = get_radius(r1=r1,r=r,d1=d1,d=d,dflt=1);
    r2 = get_radius(r1=r2,r=r,d1=d2,d=d,dflt=1);
    attachable(anchor,spin,orient, r1=r1, r2=r2, l=l, axis=BACK) {
        xrot(-90)
            linear_extrude(height=l, twist=90, scale=r2/r1, slices=20, center=true, convexity=4)
                star(n=20, r=r1, ir=r1*0.9);
        children();
    }
}
ytwistar(l=100, r1=40, r2=20) show_anchors(20);

Figure 11

Spherical Attachables

To make a spherical shape attachable, you use the r/d args of attachable().

include <BOSL2/std.scad>
module spikeball(r, d, anchor=CENTER, spin=0, orient=UP) {
    r = get_radius(r=r,d=d,dflt=1);
    attachable(anchor,spin,orient, r=r*1.1) {
        union() {
            sphere_copies(r=r, n=512, cone_ang=180) cylinder(r1=r/10, r2=0, h=r/10);
            sphere(r=r);
        }
        children();
    }
}
spikeball(r=50) show_anchors(20);

Figure 12

If the shape is an ellipsoid, you can pass a 3-item vector of sizes to r= or d=.

include <BOSL2/std.scad>
module spikeball(r, d, scale, anchor=CENTER, spin=0, orient=UP) {
    r = get_radius(r=r,d=d,dflt=1);
    attachable(anchor,spin,orient, r=r*1.1*scale) {
        union() {
            sphere_copies(r=r, n=512, scale=scale, cone_ang=180) cylinder(r1=r/10, r2=0, h=r/10);
            scale(scale) sphere(r=r);
        }
        children();
    }
}
spikeball(r=50, scale=[0.75,1,1.5]) show_anchors(20);

Figure 13

VNF Attachables

If the shape just doesn't fit into any of the above categories, and you constructed it as a VNF, you can use the VNF itself to describe the geometry with the vnf= argument.

There are two variations to how anchoring can work for VNFs. When extent=true, (the default) then a plane is projected out from the origin, perpendicularly in the direction of the anchor, to the furthest distance that intersects with the VNF shape. The anchor point is then the center of the points that still intersect that plane.

include <BOSL2/std.scad>
module stellate_cube(s=100, anchor=CENTER, spin=0, orient=UP) {
    s2 = 3 * s;
    verts = [
        [0,0,-s2*sqrt(2)/2],
        each down(s/2, p=path3d(square(s,center=true))),
        each zrot(45, p=path3d(square(s2,center=true))),
        each up(s/2, p=path3d(square(s,center=true))),
        [0,0,s2*sqrt(2)/2]
    ];
    faces = [
        [0,2,1], [0,3,2], [0,4,3], [0,1,4],
        [1,2,6], [1,6,9], [6,10,9], [2,10,6],
        [1,5,4], [1,9,5], [9,12,5], [5,12,4],
        [4,8,3], [4,12,8], [12,11,8], [11,3,8],
        [2,3,7], [3,11,7], [7,11,10], [2,7,10],
        [9,10,13], [10,11,13], [11,12,13], [12,9,13]
    ];
    vnf = [verts, faces];
    attachable(anchor,spin,orient, vnf=vnf) {
        vnf_polyhedron(vnf);
        children();
    }
}
stellate_cube(25) {
    attach(UP+RIGHT) {
        anchor_arrow(20);
        %cube([100,100,0.1],center=true);
    }
}

Figure 14

When extent=false, then the anchor point will be the furthest intersection of the VNF with the anchor ray from the origin. The orientation of the anchor point will be the normal of the face at the intersection. If the intersection is at an edge or corner, then the orientation will bisect the angles between the faces.

include <BOSL2/std.scad>
module stellate_cube(s=100, anchor=CENTER, spin=0, orient=UP) {
    s2 = 3 * s;
    verts = [
        [0,0,-s2*sqrt(2)/2],
        each down(s/2, p=path3d(square(s,center=true))),
        each zrot(45, p=path3d(square(s2,center=true))),
        each up(s/2, p=path3d(square(s,center=true))),
        [0,0,s2*sqrt(2)/2]
    ];
    faces = [
        [0,2,1], [0,3,2], [0,4,3], [0,1,4],
        [1,2,6], [1,6,9], [6,10,9], [2,10,6],
        [1,5,4], [1,9,5], [9,12,5], [5,12,4],
        [4,8,3], [4,12,8], [12,11,8], [11,3,8],
        [2,3,7], [3,11,7], [7,11,10], [2,7,10],
        [9,10,13], [10,11,13], [11,12,13], [12,9,13]
    ];
    vnf = [verts, faces];
    attachable(anchor,spin,orient, vnf=vnf, extent=false) {
        vnf_polyhedron(vnf);
        children();
    }
}
stellate_cube() show_anchors(50);

Figure 15

include <BOSL2/std.scad>
$fn=32;
R = difference(circle(10), right(2, circle(9)));
linear_sweep(R,height=10,atype="hull")
    attach(RIGHT) anchor_arrow();

Figure 16

Making Named Anchors

While vector anchors are often useful, sometimes there are logically extra attachment points that aren't on the perimeter of the shape. This is what named string anchors are for. For example, the teardrop() shape uses a cylindrical geometry for it's vector anchors, but it also provides a named anchor "cap" that is at the tip of the hat of the teardrop shape.

Named anchors are passed as an array of named_anchor()s to the anchors= argument of attachable(). The named_anchor() call takes a name string, a positional point, an orientation vector, and a spin. The name is the name of the anchor. The positional point is where the anchor point is at. The orientation vector is the direction that a child attached at that anchor point should be oriented. The spin is the number of degrees that an attached child should be rotated counter-clockwise around the orientation vector. Spin is optional, and defaults to 0.

To make a simple attachable shape similar to a teardrop() that provides a "cap" anchor, you may define it like this:

include <BOSL2/std.scad>
module raindrop(r, thick, anchor=CENTER, spin=0, orient=UP) {
    anchors = [
        named_anchor("cap", [0,r/sin(45),0], BACK, 0)
    ];
    attachable(anchor,spin,orient, r=r, l=thick, anchors=anchors) {
        linear_extrude(height=thick, center=true) {
            circle(r=r);
            back(r*sin(45)) zrot(45) square(r, center=true);
        }
        children();
    }
}
raindrop(r=25, thick=20, anchor="cap");

Figure 17

If you want multiple named anchors, just add them to the list of anchors:

include <BOSL2/std.scad>
module raindrop(r, thick, anchor=CENTER, spin=0, orient=UP) {
    anchors = [
        named_anchor("captop", [0,r/sin(45), thick/2], BACK+UP,   0),
        named_anchor("cap",    [0,r/sin(45), 0      ], BACK,      0),
        named_anchor("capbot", [0,r/sin(45),-thick/2], BACK+DOWN, 0)
    ];
    attachable(anchor,spin,orient, r=r, l=thick, anchors=anchors) {
        linear_extrude(height=thick, center=true) {
            circle(r=r);
            back(r*sin(45)) zrot(45) square(r, center=true);
        }
        children();
    }
}
raindrop(r=15, thick=10) show_anchors();

Figure 18

Sometimes the named anchor you want to add may be at a point that is reached through a complicated set of translations and rotations. One quick way to calculate that point is to reproduce those transformations in a transformation matrix chain. This is simplified by how you can use the function forms of almost all the transformation modules to get the transformation matrices, and chain them together with matrix multiplication. For example, if you have:

scale([1.1, 1.2, 1.3]) xrot(15) zrot(25) right(20) sphere(d=1);

and you want to calculate the center point of the sphere, you can do it like:

sphere_pt = apply(
    scale([1.1, 1.2, 1.3]) * xrot(15) * zrot(25) * right(20),
    [0,0,0]
);

Overriding Standard Anchors

Sometimes you may want to use the standard anchors but override some of them. Returning to the square barebell example above, the anchors at the right and left sides are on the cubes at each end, but the anchors at x=0 are in floating in space. For prismoidal/cubic anchors in 3D and trapezoidal/rectangular anchors in 2D we can override a single anchor by specifying the override option and giving the anchor that is being overridden, and then the replacement in the form [position, direction, spin]. Most often you will only want to 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:

include<BOSL2/std.scad>
module cubic_barbell(s=100, anchor=CENTER, spin=0, orient=UP) {
    override = [
                 [FWD,  [[0,-s/8,0]]]
               ];
    attachable(anchor,spin,orient, size=[s*3,s,s],override=override) {
        union() {
            xcopies(2*s) cube(s, center=true);
            xcyl(h=2*s, d=s/4);
        }
        children();
    }
}
cubic_barbell(100) show_anchors(60);

Figure 19

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:

include<BOSL2/std.scad>
module cubic_barbell(s=100, anchor=CENTER, spin=0, orient=UP) {
    override = [
                 [FWD,  [[0,-s/8,0], FWD+LEFT, 225]]
               ];
    attachable(anchor,spin,orient, size=[s*3,s,s],override=override) {
        union() {
            xcopies(2*s) cube(s, center=true);
            xcyl(h=2*s, d=s/4);
        }
        children();
    }
}
cubic_barbell(100) show_anchors(60);

Figure 20

In the above example we give three values for the override. As before, the first one places the anchor on the cylinder. We have added the second entry which points the anchor off to the left. 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:

include<BOSL2/std.scad>
module cubic_barbell(s=100, anchor=CENTER, spin=0, orient=UP) {
    override = [
                 for(j=[-1:1:1], k=[-1:1:1])
                   if ([j,k]!=[0,0]) [[0,j,k], [s/8*unit([0,j,k])]]
               ];
    attachable(anchor,spin,orient, size=[s*3,s,s],override=override) {
        union() {
            xcopies(2*s) cube(s, center=true);
            xcyl(h=2*s, d=s/4);
        }
        children();
    }
}
cubic_barbell(100) show_anchors(30);

Figure 21

Now all of the anchors in the middle are all rooted to the cylinder. Another way to do the same thing is to use a function literal for override. It will be called with the anchor as its argument and needs to return undef to just use 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:

include<BOSL2/std.scad>
module cubic_barbell(s=100, anchor=CENTER, spin=0, orient=UP) {
    override = function (anchor) 
          anchor.x!=0 || anchor==CTR ? undef  // Keep these
        : [s/8*unit(anchor)];
    attachable(anchor,spin,orient, size=[s*3,s,s],override=override) {
        union() {
            xcopies(2*s) cube(s, center=true);
            xcyl(h=2*s, d=s/4);
        }
        children();
    }
}
cubic_barbell(100) show_anchors(30);

Figure 22

Clone this wiki locally