3D designs

This page contains various 3D designs I've made for either 3D printing or woodwork. There are more not listed here, but they are so application-specific they are bound not to be useful to anyone else. I hereby release these designs into the public domain - feel free to reuse these as you like.

Note that I typically write CAD designs as Python or SCAD programs, rather than construct them with CAD programs. This approach seems to interface with my brain better than the CAD GUIs I've used, but I know I'm weird (saying that, I would probably still use a GUI for a complex, mission critical design). If you want to reuse any of these designs you'll have to be (or become) acquainted with OpenSCAD or CadQuery.

A key

A 3D printed key.

A 3D printed key.

I needed another key for the basement. The key was a very simple design so I figured I didn't need a professional copy. I printed this using carbon fibre reinforced PLA filament to give it extra strength, and ensured that it was sliced in such a way that the weak axis (vertical as printed) was not along the barrel, where significant shear forces will be occur during locking/unlocking. The printed key works nicely and hasn't yet broken after a few hundred lockings and unlockings.

Code (OpenSCAD):

$fn = 100;

// Barrel.
cylinder(50, 3.25, 3.25);

// Handle.
translate([-10, -3.25, -20]) difference() {
    cube([20, 6.5, 20]);
    translate([5, -1, 5]) cube([10, 22, 10]);
}

// Bit.
translate([0, -1, 36.5]) cube([7, 2, 9]);
translate([6, -3, 36.5])
    linear_extrude(9)
        polygon([[0, 6], [0, 0], [10, 0], [10, 6], [8, 6], [8, 2], [2, 2], [2, 6]]);

Bubble blower

A bubble blower.

A bubble blower.

Who doesn't want to blow bubbles?

Code (OpenSCAD):

$fn = 100;

rotate_extrude(convexity = 2)
translate([15, 0, 0])
circle(r = 1.5);

rotate([90, 0, 0]) translate([0, 0, 15]) cylinder(75, 1.5, 1.5);

Heart soap mould

A heart shaped soap mould.

A heart shaped soap mould.

This is a heart shaped mould that I made for my soap making enthusiast partner.

Code (CadQuery):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import cadquery as cq

outer = (
    cq.Workplane("XY")
    .lineTo(2, 2)
    .threePointArc((4, 1), (3.5, 0))
    .mirrorX()
    .extrude(-2)
    .edges("<Z")
    .fillet(1.2)
)
inner = (
    cq.Workplane("XY")
    .lineTo(2, 2)
    .threePointArc((4, 1), (3.5, 0))
    .mirrorX()
    .offset2D(-0.075, kind="intersection")
    .extrude(-1.925)
    .edges("<Z")
    .fillet(1.125)
)

heart = outer - inner
show_object(heart, name="heart")

Entrance bag stand

A wooden bag stand for an entrance.

A wooden bag stand for an entrance.

A moderate woodwork project for a bag stand. Contains mortise-and-tenon slats, a nice table top and sturdy legs.

When I modelled this, I also added the hall and nearby doors to check it would fit.

Photo of the completed construction:

The completed wooden bag stand.

The completed wooden bag stand. Yes, we're messy.

Code (OpenSCAD):

leg_thickness = 40;
leg_height = 700;
lower_stretcher_y_offset = 100; // Lower edge.
stretcher_width = 20;
stretcher_height = 40;
stretcher_mortise_width = 10;
stretcher_mortise_depth = 10;
stretcher_mortise_height = 20;
carriage_slat_width = 40;
carriage_slat_height = 10;
carriage_slat_mortise_width = 30;
carriage_slat_mortise_depth = 10;
carriage_slat_mortise_height = 5;
top_plank_height = 20;
n_slats = 6;

width = 700;
depth = 250;

module leg() {
    difference() {
        cube([leg_thickness, leg_thickness, leg_height]);
        // Short edge mortises.
        // Upper.
        translate([leg_thickness/2, leg_thickness, leg_height-stretcher_height/2])
            cube(
                [stretcher_mortise_width, stretcher_mortise_depth*2, stretcher_mortise_height],
                center = true
            );
        // Lower.
        translate([leg_thickness/2, leg_thickness, lower_stretcher_y_offset])
            cube(
                [stretcher_mortise_width, stretcher_mortise_depth*2, stretcher_mortise_height],
                center = true
            );
        // Long edge mortises.
        // Upper.
        translate([leg_thickness, leg_thickness/2, leg_height-stretcher_height/2])
            cube(
                [stretcher_mortise_depth*2, stretcher_mortise_width, stretcher_mortise_height],
                center = true
            );
        // Lower.
        translate([leg_thickness, leg_thickness/2, lower_stretcher_y_offset])
            cube(
                [stretcher_mortise_depth*2, stretcher_mortise_width, stretcher_mortise_height],
                center = true
            );
    }
}

module slat(width, depth, height, tenon_width, tenon_depth, tenon_height) {
    union() {
        cube([width, depth, height]);
        translate([width/2-tenon_width/2, -tenon_depth, height/2-tenon_height/2])
            cube([tenon_width, tenon_depth, tenon_height]);
        translate([width/2-tenon_width/2, depth, height/2-tenon_height/2])
            cube([tenon_width, tenon_depth, tenon_height]);
    }
}

module stretcher_short() {
    slat(
        stretcher_width,
        depth - leg_thickness,
        stretcher_height,
        stretcher_mortise_width,
        stretcher_mortise_depth,
        stretcher_mortise_height
    );
}

module slat_mortises() {
    for(i = [1:n_slats]) {
        translate(
            [
                -stretcher_width / 2 + carriage_slat_mortise_depth - 1,
                (width - 2 * leg_thickness) / (2 * n_slats) * (2 * i - 1) - carriage_slat_mortise_width / 2,
                stretcher_height / 2 - carriage_slat_height / 2 + carriage_slat_mortise_height / 2
            ]
        )
            cube(
                [
                    carriage_slat_mortise_depth,
                    carriage_slat_mortise_width,
                    carriage_slat_mortise_height
                ]
            );
    }
}

module stretcher_long() {
    difference() {
        slat(
            stretcher_width,
            width - 2 * leg_thickness,
            stretcher_height,
            stretcher_mortise_width,
            stretcher_mortise_depth,
            stretcher_mortise_height
        );
        slat_mortises();
    }
}

module carriage_slat() {
    slat(
        carriage_slat_width,
        depth,
        carriage_slat_height,
        carriage_slat_mortise_width,
        carriage_slat_mortise_depth,
        carriage_slat_mortise_height
    );
}

module side() {
    translate([0, 0, 0])
        leg();
    rotate([0, 0, -90])
        translate([-depth-leg_thickness, 0, 0])
            leg();

    // Lower stretcher.
    translate(
        [
            leg_thickness / 2 - stretcher_width / 2,
            leg_thickness,
            lower_stretcher_y_offset - stretcher_height / 2
        ]
    )
        stretcher_short();
    // Upper stretcher.
    translate(
        [
            leg_thickness / 2 - stretcher_width / 2,
            leg_thickness,
            leg_height - stretcher_height
        ]
    )
        stretcher_short();
}

module carriage() {
    rotate([0, 0, -90])
        // Left/back.
        stretcher_long();
    rotate([0, 0, 90])
        // Right/front.
        translate([depth - leg_thickness / 2, -width + 2 * leg_thickness, 0])
            stretcher_long();
    // Slats.
    for (i = [1:n_slats]) {
        translate(
            [
                (width - 2 * leg_thickness) / (2 * n_slats) * (2 * i - 1) - carriage_slat_width / 2,
                -stretcher_width / 2,
                stretcher_height / 2 - carriage_slat_height / 2
            ]
        )
        carriage_slat();
    }
}

module top() {
    rotate([0, 0, -90])
        // Back.
        stretcher_long();
    rotate([0, 0, -90])
        // Front.
        translate([-depth, 0, 0])
            stretcher_long();
}

module table_top() {
    cube([width+leg_thickness, depth+2*leg_thickness, top_plank_height]);
}

color("yellow")
    // Left side.
    side();
    // Right side.
    translate([width, depth+leg_thickness, 0])
        rotate([0, 0, 180])
            side();
    // Carriage.
    translate(
        [leg_thickness, leg_thickness-stretcher_width/2, lower_stretcher_y_offset-stretcher_height/2]
    )
        carriage();
    // Top.
    translate([leg_thickness, leg_thickness-stretcher_width/2, leg_height-stretcher_height])
        top();

    translate([-leg_thickness/2, -leg_thickness/2, leg_height])
        table_top();

// Walls.
color("white")
    translate([-1120, depth+70, 0])
        cube([2300, 1, 2500]);

color("white")
    rotate([0, 0, 90])
        translate([-750, -1160, 0])
            cube([1070, 1, 2500]);

// Doors.
color("grey")
    translate([-920, depth+65, 0])
        cube([860, 30, 2000]);

color("grey")
    rotate([0, 0, 180])
        translate([-1160, -100, 0])
            rotate([0, 0, 30])
                cube([750, 30, 2000]);

Matchstick light

A light holder made from strips of rectangular profile wood.

A light holder made from strips of rectangular profile wood.

A cool light holder made from rectangular wood. I've not built this large version yet, but I did make a smaller version:

A small matchstick light.

The smaller version of the matchstick light that I made.

Code (CadQuery):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
from functools import reduce
from collections import defaultdict
import cadquery as cq

sticks_per_side = 7
stick_side_length = 14
layers = 70

x_length = (2*sticks_per_side+1)*stick_side_length
y_length = 2*sticks_per_side*stick_side_length

bom = defaultdict(lambda: 0)

def long_stick():
    bom["long"] += 1

    return (
        cq.Workplane("XY")
        .box(x_length, stick_side_length, stick_side_length)
        .translate((x_length/2, 0, 0))
    )

def short_stick():
    bom["short"] += 1

    length = 2*stick_side_length
    return (
        cq.Workplane("XY")
        .box(length, stick_side_length, stick_side_length)
        .translate((length/2, 0, 0))
    )

def layer():
    inner1 = reduce(
        lambda a, b: a + b,
        [
            short_stick().translate((0, 2*(n+1)*stick_side_length, 0))
            for n in range(sticks_per_side - 2)
        ]
    )

    inner2 = reduce(
        lambda a, b: a + b,
        [
            short_stick().translate((x_length - 2*stick_side_length, 2*(n+1)*stick_side_length, 0))
            for n in range(sticks_per_side - 2)
        ]
    )

    return (
        long_stick()
        + inner1
        + inner2
        + long_stick().translate((0, y_length - 2*stick_side_length, 0))
    )

def layer_stack():
    return reduce(
        lambda a, b: a + b,
        [
            layer().rotateAboutCenter((0, 0, 1), 90*(n%2)).translate((0, 0, n*stick_side_length))
            for n in range(layers)
        ]
    )

part = layer_stack()

total_short = bom['short']*2*stick_side_length
total_long = bom['long']*x_length
total = total_short + total_long

log(f"{bom['short']} short sticks = {total_short}")
log(f"{bom['long']} long sticks = {total_long}")
log(f"Total: {total}")

show_object(part, name="Light")