All-in-one Watercolor Palette

3D-printed parametric palette

As we’re getting more comfortable with our 3D printer, we’re starting to look out for handy things to print, along with nice modelling/design software. This is a short post describing how to use OpenSCAD‘s python scripting via SolidPython to design a 3D-printed box that holds watercolor paint tubes, a brush, a sponge, and has wells for mixing paints. It’s based on a small set of Daniel Smith watercolor tubes and a Sakura Koi watercolor brush, but can probably be used with other brands with minor adjustments.

Dimensions

  • Each paint tube is approx. 1.8cm x 6.2cm x 1.3cm. There are 6 tubes in total, so two tube holders of 5cm x 7cm x 1.5cm should fit them. It’s only 5cm long (and not 1.8 x 3) because, as in the figure below, you can place three of them in an alternating fashion and save some space.

daniel smith tubes

In [1]:
# x, y, z = length, width, height
tube_holder_length = 5
tube_holder_width = 7
tube_holder_height = 1.5
  • We decided on a 0.2mm buffer everywhere (increasing this to 0.5 ups the printing time by around 2 hours and any less would probably be hard to print)
In [2]:
gap = 0.2
  • And then a smaller water holder of 5cm x 2cm x 1.5cm in between the two tube holder boxes
In [3]:
water_holder_length = 5
water_holder_width = 2
water_holder_height = 1.5
  • We have a couple of watercolor brushes and all of them are shorter than 16cm so that’s nice. The widest one is around 1.8cm wide so a brush holder of 2cm x 16.4cm x 1.5cm should fit even this one.
In [4]:
brush_holder_length = 2
brush_holder_width = 16.4
brush_holder_height = 1.5
  • Finally in between the tube holders and the brush holder there’s space for four mixing wells (1.5cm radius half-spheres) and a sponge holder of 3cm x 3cm x 1.5cm.
In [5]:
sponge_holder_length = 3
sponge_holder_width = 3
sponge_holder_height = 1.5

num_mixing_wells = 4
mixing_well_radius = 1.5
In [6]:
total_length = round(gap + tube_holder_length + gap + sponge_holder_length + gap + brush_holder_length + gap, 3)
total_width = round(gap + tube_holder_width + gap + water_holder_width + gap + tube_holder_width + gap, 3)
total_height = round(gap + tube_holder_height, 3)

total_length, total_width, total_height
Out[6]:
(10.8, 16.8, 1.7)

With all that in mind, let’s code it out with SolidPython, a python scripting wrapper for OpenSCAD. We also used the ViewSCAD library to render the models. The nice thing about SolidPython is that all the normal mathematical operations are overloaded in an intuitive way to respond to 3D objects. This means you can subtract a sphere from a box and get a mixing well. This is better demonstrated by example.

In [7]:
import solid 
import viewscad
import numpy as np

renderer = viewscad.Renderer()

The base

We start out with a foundation from which we’ll carve out our holders.

In [8]:
watercolor_palette = solid.cube((total_length, total_width, total_height))
renderer.render(watercolor_palette)

palette base

Tube holders

In [9]:
tube_holder = solid.cube((tube_holder_length, tube_holder_width, tube_holder_height))

first_tube_holder = solid.translate((gap, gap, gap))(tube_holder)
second_tube_holder = solid.translate((gap, 
                                      gap + tube_holder_width + gap + water_holder_width + gap, 
                                      gap))(tube_holder)

watercolor_palette -= first_tube_holder
watercolor_palette -= second_tube_holder

renderer.render(watercolor_palette)

palette with tube holders

Water holder

In [10]:
water_holder = solid.cube((water_holder_length, water_holder_width, water_holder_height))
water_holder = solid.translate((gap, 
                                gap + tube_holder_width + gap, 
                                gap))(water_holder)

watercolor_palette -= water_holder
renderer.render(watercolor_palette)

palette with tube holders and water holder

Brush holder

In [11]:
brush_holder = solid.cube((brush_holder_length, brush_holder_width, brush_holder_height))
brush_holder = solid.translate((gap + tube_holder_length + gap + sponge_holder_length + gap, 
                                gap, 
                                gap))(brush_holder)

watercolor_palette -= brush_holder
renderer.render(watercolor_palette)

palette with tube holders, water holder, and brush holder

Sponge holder

In [12]:
sponge_holder = solid.cube((sponge_holder_length, sponge_holder_width, sponge_holder_height))
sponge_holder = solid.translate((gap + tube_holder_length + gap, 
                                 gap, 
                                 gap))(sponge_holder)

watercolor_palette -= sponge_holder
renderer.render(watercolor_palette)

palette with tube holders, water holder, brush holder and sponge holder

Mixing wells

We used 75 segments as a guess, it worked out well enough in the end. Since it’s a sphere and not a cube, you now have to give the offsets with respect to the center of the sphere.

In [13]:
sphere = solid.sphere(r=mixing_well_radius, segments=75)
spheres = sum([solid.translate((gap + tube_holder_length + gap + mixing_well_radius, 
                                y, 
                                gap + mixing_well_radius))(sphere) 
               for y in np.linspace(gap + sponge_holder_width + gap + mixing_well_radius,
                                    total_width - gap - mixing_well_radius,
                                    num_mixing_wells)])

watercolor_palette -= spheres
renderer.render(watercolor_palette)

finished palette design

Exporting

If you want to play around with this in the OpenSCAD GUI then SolidPython has a function for that. The output can be directly loaded into OpenSCAD.

In [14]:
print(solid.scad_render(watercolor_palette))

difference() {
	cube(size = [10.8000000000, 16.8000000000, 1.7000000000]);
	translate(v = [0.2000000000, 0.2000000000, 0.2000000000]) {
		cube(size = [5, 7, 1.5000000000]);
	}
	translate(v = [0.2000000000, 9.6000000000, 0.2000000000]) {
		cube(size = [5, 7, 1.5000000000]);
	}
	translate(v = [0.2000000000, 7.4000000000, 0.2000000000]) {
		cube(size = [5, 2, 1.5000000000]);
	}
	translate(v = [8.6000000000, 0.2000000000, 0.2000000000]) {
		cube(size = [2, 16.4000000000, 1.5000000000]);
	}
	translate(v = [5.4000000000, 0.2000000000, 0.2000000000]) {
		cube(size = [3, 3, 1.5000000000]);
	}
	union() {
		translate(v = [6.9000000000, 4.9, 1.7000000000]) {
			sphere($fn = 75, r = 1.5000000000);
		}
		translate(v = [6.9000000000, 8.3, 1.7000000000]) {
			sphere($fn = 75, r = 1.5000000000);
		}
		translate(v = [6.9000000000, 11.700000000000001, 1.7000000000]) {
			sphere($fn = 75, r = 1.5000000000);
		}
		translate(v = [6.9000000000, 15.100000000000001, 1.7000000000]) {
			sphere($fn = 75, r = 1.5000000000);
		}
	}
}

But we were happy with this as is, so we just wrote it out to an STL file which we loaded into Ultimaker Cura for slicing.

In [ ]:
renderer.render(watercolor_palette, outfile="watercolor_palette.stl")

3D printing and results

We used the Creality Ender 3 profile, 0.28 mm resolution, 20% infill, no supports, and no adhesion. It took around 8 hours to print. Here’s the final print, in all its functional glory:

Next steps

We’ll try to add a lid to this so it can be carried around.



For comments, click the arrow at the top right corner.