In this project we want to construct a parameterizable fan wheel, in which the angle of attack and the torsion of the blades are adjustable in particular.
We will learn about a new property of the linear_extrude transformation and use the mathematical functions abs and atan.
Let’s start with the definition of a module fan_wheel, a set of parameters and a submodule blade, where we will describe the geometry of a single blade of the fan wheel:
module fan_wheel(
outer_radius = 55,
inner_radius = 15,
blade_count = 8,
outer_width = 50,
inner_width = 20,
thickness = 2,
twist = -30,
angle_of_attack = 45,
){
module blade() {
}
blade(); // debug
}
fan_wheel();
Except for the parameter blade_count, which sets the number of fan blades, all other parameters refer to geometrical properties of a single blade. Figure 12. shows schematically which properties of a blade are refered to by the parameters outer_radius, inner_radius, outer_width and inner_width. We will derive the values inner_height and blade_height in figure 12. later. The parameters twist and angle_of_attack affect the twist of the fan blade and the angle of the blade at the fan hub. The parameter thickness sets the thickness of the fan blade.
Inside the module fan_wheel we have created the submodule blade as well as a test instance of this submodule. Let’s start with the geometry description of a single blade:
module fan_wheel(
/* ... */
){
module blade() {
inner_height =
cos(
asin( inner_width / ( 2 * inner_radius ) )
) * inner_radius;
blade_height = outer_radius - inner_height;
translate( [0, inner_height, 0] )
rotate( [-90, 0, 0] )
linear_extrude(
height = blade_height,
twist = twist,
slices = 10 ,
convexity = 10
)
square( [outer_width, thickness], center = true );
}
blade(); // debug
}
/* ... */
We first calculate the values inner_height and blade_height (Figure 12.). The inner_height is the point where the vertical with distance inner_width / 2
from the origin and the circle with inner_radius meet. To calculate the inner_height we need the angle between the radius and the vertical at this intersection (asin( inner_width / ( 2 * inner_radius )
). Then we can determine the inner_height using the cosine and the inner_radius. The blade_height is the remainder of the distance from inner_height to outer_radius.
We model the blade itself using the 2D basic shape square and a linear extrusion. We use the twist parameter of linear_extrude to rotate the basic shape during extrusion (Figure 12.). The parameter slices can be used to set how many subdivisions linear_extrude should make along the extrusion to model the twist. The parameter convexity is an auxiliary parameter that ensures that the preview of the geometry is calculated cleanly and has no holes. By default, this parameter has a value of 1. If you find errors in the geometry during preview, it is worth trying to increase this parameter. A value of 10 should be sufficient in practically all cases.
Now we will describe the tapered shape of the fan blade using a Boolean difference operation:
module fan_wheel(
/* ... */
){
module blade() {
/* ... */
angle =
atan(
( ( outer_width / 2 * cos( abs( twist ) ) ) -
( inner_width / 2 )
) / blade_height
);
height = sin( abs( twist ) ) * outer_width / 2 +
thickness;
width = outer_width / 2 - inner_width / 2;
depth =
sqrt(
pow( ( outer_width / 2 * cos( abs( twist ) ) ) -
( inner_width / 2 ), 2 ) +
pow( blade_height, 2 )
) + thickness;
difference(){
translate( [0, inner_height, 0] )
rotate( [-90, 0, 0] )
linear_extrude(
height = blade_height,
twist = twist,
slices = 10 ,
convexity = 10
)
square( [outer_width, thickness], center = true );
for (i = [0 : 1])
mirror( [i, 0, 0] )
translate([
inner_width / 2,
inner_height,
-height
])
rotate( [0, 0, -angle] )
translate( [0, -depth / 2, 0] )
cube( [width * 2, depth * 1.5, 2 * height] );
}
}
blade(); // debug
}
/* ... */
We first calculate the angle of the side via the arc tangent function atan. It gets as parameter the difference between inner_width and the outer_width rotated by twist. We use the absolute value (abs) of twist in this case to get the same result even with a negative twist parameter. Afterwards we determine the minimal height, width and depth of the box (cube) that we want to subtract from the fan blade.
After these preparations, we can now move our fan blade description into the geometry set of a Boolean difference operation and define two boxes that we subtract from the fan blade. Since the sides are mirror symmetric, we can create the two boxes using a combination of for-loop and mirror transformation. We make the box that will be subtracted slightly larger than generally necessary. This way, we also cover situations that may arise due to an “extreme” parameterization of the fan wheel (for example, when the outer_width is very small). Figure 12. shows the position of the described boxes. It also shows that now the sides of our fan blade have become jagged. This jagged structure comes from the fact that the basic 2D shape of our blade is a very thin rectangle, which is internally formed by only two triangles. These are now no longer sufficient to cleanly describe the slanted side edges of the fan blade.
We can fix this problem by using a polygon as 2D base shape instead of a simple rectangle. We need to design the polygon in such a way that it has enough internal “resolution”. To do this, we need to use a little trick:
module fan_wheel(
/* ... */
){
module blade() {
/* ... */
blade_divisions = 10;
difference() {
translate( [0, inner_height, 0] )
rotate( [-90, 0, 0] )
linear_extrude(
height = blade_height,
twist = twist,
slices = 10 ,
convexity = 10
)
translate([
-outer_width / 2,
-thickness / 2
])
polygon( concat(
[for (i = [0 : blade_divisions])
[i * outer_width / blade_divisions, (i % 2) * 0.0001]
],
[for (i = [blade_divisions : -1 : 0])
[i * outer_width / blade_divisions,
thickness + (i % 2) * 0.0001]
]
));
/* ... */
} // difference
}
blade(); // debug
}
/* ... */
We create the polygon from two concatenated (concat) arrays. The first array describes the points of the polygon in X-direction at height 0. The second array describes the points of the polygon in opposite X-direction at height thickness. If all our points in forward and backward direction exactly lie on a line, then OpenSCAD will optimize these points away. To prevent this from happening, we need to move every other point minimally in the Y-direction. We use the modulo operation for this purpose. The expression ‘i % 2’ alternates between 0 and 1 for consecutive i. Our Y-coordinate therefore jumps back and forth from point to point between 0 and 0.0001. By doing this, we prevent OpenSCAD from optimizing and achieve the desired goal of a higher geometric resolution of our 2D base shape. Figure 12. shows how this eliminates the jagged sides of the fan blade.
Our fan blade is now almost finished. We only have to rotate it by its angle_of_attack and add the outer and inner radius to the geometry description. For the outer radius we can do this with a Boolean intersection and for the inner radius we can use another Boolean difference:
module fan_wheel(
/* ... */
){
module blade() {
/* ... */
difference(){
intersection(){
rotate( [0, -angle_of_attack, 0] )
difference() {
/* ... */
}
// outer radius
translate( [0, 0, -height] )
cylinder(
r = outer_radius,
h = 2 * height,
$fn = 100
);
}
// inner radius
translate( [0, 0, -height] )
cylinder( r = inner_radius, h = 2 * height, $fn = 50);
}
}
blade(); // debug
}
/* ... */
We apply the intersection and difference operation only after the rotation by angle_of_attack so that the outer and inner edges of the fan blade remain vertical regardless of the rotation. This completely describes our submodule blade (Figure 12.).
The completion of the main module is now relatively straightforward:
module fan_wheel(
/* ... */
){
module blade() {
}
// hub
hub_height =
sin( abs( angle_of_attack ) ) * inner_width / 2 +
thickness;
translate( [0, 0, -hub_height] )
cylinder( r = inner_radius, h = hub_height * 2, $fn = 50);
// blades
for(i = [0 : 360 / blade_count : 359])
rotate( [0, 0, i] )
blade();
}
/* ... */
We first determine the height of our hub (hub_height) depending on the angle of attack, the inner width and the material thickness. Then we create the hub as a simple cylinder and center this cylinder along the Z-axis. We now describe the blades using a for-loop, where the step size of the loop variable i results from the parameterized number of blades (360 / blade_count
). We use the loop variable i to distribute our submodule blade with an appropriate number of copies over 360 degrees by means of rotation around the Z axis (rotate( [0, 0, i] )
).
With this our fan wheel is finished (Figure 12.)!
The blades of the fan wheel have a strong overhang, especially towards the outside. Here it is recommended to print with a low layer height and a larger line width. If your 3D printer has a 0.4mm print nozzle, you can print lines with a width of 0.5mm to 0.6mm without problems. As layer thickness you can go down to 0.075mm with most printers. Another trick is to slow down the print speed for the outer walls of the print. This allows the filament to cool down more as it is exposed to the print head blower for longer.
If you print the fan wheel with support structures, you should activate the option for forming a support roof. This will make the interface between the support structure and the 3D model cleaner. An alternative to using support structures is to split the fan wheel into an upper and a lower half using a Boolean intersection:
// upper half
intersection() {
fan_wheel();
translate([-100,-100,0])
cube([200,200,100]);
}
// lower half
translate([120,0,0])
rotate([180,0,0])
intersection() {
fan_wheel();
translate([-100,-100,-100])
cube([200,200,100]);
}
After printing both halves, they can then be glued together. This can be done, for example, with superglue and activator, or with a two-component adhesive. To ensure a clean alignment of the two halves, it can be helpful to drill two holes in the hub using a Boolean difference operation and to insert two pins in these holes and use them as alignment aids when gluing.