Neale Pickett
·
2025-12-13
whistle.scad
1include <common.scad>
2include <tonehole.scad>
3
4// Which section to render for printing
5Section = -1; // [-1:Everything, 0:Bottom, 1:Middle, 2:Top]
6
7// Height of the instrument
8Height = 469;
9// Inner bore diameter
10InnerDiameter = 20; // 0.1
11// Outer diameter
12OuterDiameter = 25.0; // 0.1
13
14// Diameter of the rubber O ring
15ORingDiameter = 1.50; // 0.01
16// How far beyond the outer diameter the O ring should protrude
17ORingProtrusion = 0.80; // 0.01
18
19// Cuts to make for a sectioned print
20Cuts = [190, 340];
21
22// Use Axianov tone holes?
23AxianovHoles = true;
24
25// Text to display on the back
26Text = "d";
27
28Hole1 = [90, 7.45];
29Hole2 = [130, 11.0];
30Hole3 = [161, 9.45];
31Hole4 = [221, 7.45];
32Hole5 = [254, 9.50];
33Hole6 = [293, 7.45];
34
35/* [ Esoterics ] */
36// Angle for tone holes. Anything other than 45 will create challenging overhangs!
37AxianovHoleAngle = 45;
38
39// Depth of the tenon joint. Shallower will be less stable.
40TenonDepth = 20;
41
42// Make the inner bore square, outer body hexagonal?
43LowPoly = false;
44
45/* [ Hidden ] */
46sortedCuts = [min(Cuts[0], Cuts[1]), max(Cuts[0], Cuts[1])];
47id = LowPoly ? (sqrt(TAU) * InnerDiameter/2) : InnerDiameter;
48od = LowPoly? (sqrt(TAU) * OuterDiameter/2) : OuterDiameter;
49
50module whistle() {
51 difference() {
52 if (LowPoly) {
53 union() {
54 cylinder(h=Height-70, d=od, $fn=6);
55 translate([0, 0, Height-70]) cylinder(h=70, d=OuterDiameter);
56 }
57 } else {
58 cylinder(h=Height, d=od);
59 }
60
61 translate([0, 0, -1]) {
62 if (LowPoly) {
63 cylinder(h=Height+2, d=id, $fn=4);
64 } else {
65 cylinder(h=Height+2, d=InnerDiameter);
66 }
67 }
68
69 // O ring grooves
70 for (z = [Height-7, Height-20]) {
71 translate([0, 0, z]) tube(h=ORingDiameter*1.3, id=OuterDiameter - ORingDiameter*2 + ORingProtrusion, od=OuterDiameter+1);
72 }
73
74 // Decoration
75 for (i = [0, sortedCuts[0], sortedCuts[1]]) {
76 translate([0, 0, i + 2]) {
77 translate([0, 0, 0]) tube(h=1, id=OuterDiameter-0.5, od=OuterDiameter*2);
78 translate([0, 0, 4]) tube(h=1, id=OuterDiameter-0.5, od=OuterDiameter*2);
79 }
80 }
81
82 for (hole = [Hole1, Hole2, Hole3, Hole4, Hole5, Hole6]) {
83 if (AxianovHoles) {
84 z = hole[0];
85 top = (z>sortedCuts[0]) || (z>sortedCuts[1]);
86 m = [0, top?0:1, 0];
87 r = [90, top?90-AxianovHoleAngle:90+AxianovHoleAngle, 0];
88 translate([0, 0, hole[0]]) rotate(r) mirror(m) tonehole(d=hole[1]);
89 } else {
90 translate([0, 0, hole[0]]) rotate([90, 0, 0]) cylinder(d=hole[1], h=50);
91 }
92 }
93
94 // Ridges to catch seams
95 rotate([0, 0, 110]) translate([0, OuterDiameter/2, 0]) cylinder(h=sortedCuts[0], d=1);
96 rotate([0, 0, -110]) translate([0, OuterDiameter/2, sortedCuts[0]]) cylinder(h=sortedCuts[1]-sortedCuts[0], d=1);
97 rotate([0, 0, 110]) translate([0, OuterDiameter/2, sortedCuts[1]]) cylinder(h=Height, d=1);
98
99 // Inner channel, seems to cause problems with the tenon join being too thin
100 //rotate([0, 0, 140]) translate([0, InnerDiameter/2, 0]) cylinder(h=Height, d=1);
101
102 // Ruby head, and text on the back
103 // This makes previews slow, so we only do it in final renders.
104 if (!$preview) {
105 intersection() {
106 tube(h=50, id=OuterDiameter-0.5, od=OuterDiameter+10);
107 union() {
108 rotate([90, 0, 0]) {
109 linear_extrude(height=OuterDiameter) {
110 translate([-5, 0, 0]) import("ruby.svg");
111 }
112 }
113 translate([0, 0, 8]) {
114 rotate([90, 0, 180]) {
115 linear_extrude(height=OuterDiameter) {
116 text(Text, halign="center");
117 }
118 }
119 }
120 }
121 }
122 }
123 }
124}
125
126sectionedCuts = concat([-1000], sortedCuts, [1000]);
127if (Section == -1) {
128 whistle();
129} else {
130 tenon(h=sectionedCuts[Section], od=OuterDiameter, depth=TenonDepth, top=true) {
131 tenon(h=sectionedCuts[Section+1], od=OuterDiameter, depth=TenonDepth, top=false) {
132 whistle();
133 }
134 }
135}