So it's past time to overhaul the sprinklers in the front yard, and that
involved building a new sprinkler manifold.
A normal person would sketch out something, go buy some parts, and start
putting them together. But that would be too easy!
Besides, I kept losing track of how many of each part I needed.
OpenSCAD to the rescue!
That's a sprinkler manifold, with automatic valves.
Once I got the components built, describing the plumbing was easy. You
start with the source of the water, and for each component the
downstream components are its children. Thus, e.g.:
pipe(length=100) elbow() pipe(length=100);
yields:
Tees are a little trickier, but not much: they have two children.
Note that you can only really model tree structures (like real
plumbing); there's no mechanism for figuring out how to reconnect to
previously defined plumbing.
This is not product-grade, but it let me "draw" my project and produce a
BOM.
The components come first; a couple of manifold models are at the end.
(Note that only plan2() is rendered; plan1() is an earlier idea that I
didn't end up using.)
// Plumbing modeling
// Make it "easy" to build plumbing assemblies.
// Jordan Brown 23 July 2020 openscad@jordan.maileater.net
// CAVEAT: This was built for visualization and parts lists only.
// Dimensions are approximate and are sometimes guesses.
// Do not use where precise scale is required!
// In addition to the model, this produces a bill of materials, like
// so:
// ECHO: "connector", "M", "S", "3/4"", "sch40"
// ECHO: "elbow", "S", "S", "3/4"", "sch40"
// ECHO: "angle45", "S", "S", "3/4"", "sch40"
// See the example at the bottom for how to use these modules.
// Yeah, it should be in a library. Some day.
// Note: It seems like I should specify what the orientation is for
// each connector - whether an elbow turns "left" or "right".
// But as soon as you get started, it hurts my head to even figure out
// what "left" and "right" mean, so I haven't bothered. Just add a
// part and insert whatever rotation is needed. Note that since the
// parts always start from [0,0,0] and extend into +Z, you only
// ever need a single-term rotate. (Which makes sense; the only
// rotation you can control is how the next piece attaches.)
// Except, of course, for the first part, where you need to rotate
// and perhaps translate so as to start at the right place and heading
// the right direction.
// Indexes for connection types.
SLIP=0;
PIPE=1;
MIPT=2;
FIPT=3;
labels = [ "S", "P", "M", "F" ];
// Indexes for PVC pipe sizes.
S12 = 0; // 1/2"
S34 = 1; // 3/4"
S1 = 2; // 1"
slabels = [ "1/2\"", "3/4\"", "1\"" ];
// Following dimensions are a mix of guesses and measurement.
// *_depth, *_length, *_od are indexed by the pipe size.
// depths, lengths, and ods are indexed by the type of connection.
// Depths - how far the adjacent piece slides inside this one.
slip_depth = [ 20, 20, 20 ];
mipt_depth = [ 0, 0, 0 ];
fipt_depth = [ 11, 11, 11 ];
depths = [ slip_depth, 0, mipt_depth, fipt_depth ];
// Lengths - how long the "joint" part of the component is.
slip_length = [ 20,20,20 ];
mipt_length = [ 17,17,17 ];
fipt_length = [ 20,20,20 ];
lengths = [ slip_length, 0, mipt_length, fipt_length ];
// Outside diameters of the various connectors.
slip_od = [ 18, 33, 41 ];
pipe_od = [ 15, 27, 33 ];
mipt_od = [ 16, 26, 28 ];
fipt_od = [ 18, 33, 41 ];
ods = [ slip_od, pipe_od, mipt_od, fipt_od ];
// Theoretically there could also be a list of inside diameters,
// but at the moment I'm not modeling the insides.
// Outside diameter of hosebib body. The rest of the hosebib
// dimensions are hardcoded.
hosebib_ods = [ undef, 29, undef ];
// Following are dimensions of various parts of a Champion valve and
// compact automatic actuator, for 3/4" and 1". Right now they're the
// same, which is close but not quite right.
//
// ball_d = dims[0];
// inlet_od = dims[1];
// body = dims[2];
// union_od = dims[3];
// union_h = dims[4];
// cap_d = dims[5];
// cap_h = dims[6];
// neck_h = dims[7];
// neck_d = dims[8];
// saucer_h = dims[9];
// saucer_d = dims[10];
// screw_h = dims[11];
// screw_d = dims[12];
// sol_x = dims[13];
// sol_h = dims[14];
// sol_d = dims[15];
// c_to_c = dims[16]; // spacing between centers of input and output
// union_h2 = dims[17];
// union_d2 = dims[18]; //32
// cap_x = dims[19];
// sep = dims[20]; // Separation between adjacent valves
spr_valve_dims = [
undef, // No 1/2" valves
[50, 41, [120, 30, 50], 52, 20, 52, 10, 25, 18, 20, 80, 50, 10, 25, 30, 25, 65, 16, 32, 50, 100 ], // approx
[50, 41, [120, 30, 50], 52, 20, 52, 10, 25, 18, 20, 80, 50, 10, 25, 30, 25, 65, 16, 32, 50, 100 ], // approx
];
// This is a single connector, either a slip-female, a thread-male,
// or a thread-female.
// Its [0,0,0] is where the previous part ends,
// so for either of the female types it's inside the part, where for
// the thread-male it's at the end.
// It optionally takes a child, which is any subsequent part, similarly
// positioned.
// end() should probably take the rest of the component as a child,
// so that end() can be responsible for positioning the rest of the
// component, but it doesn't; the component is responsible for that
// positioning.
// Includes a label on the connector saying what type it is.
module end(size=S34, type=SLIP) {
h = lengths[type][size];
d = ods[type][size];
// Switch here so we can render differently in the future.
if (type == SLIP) {
#cylinder(h=h, d=d);
} else if (type == MIPT) {
cylinder(h=h, d=d);
} else if (type == FIPT) {
#cylinder(h=h, d=d);
} else {
assert(false);
}
translate([0,0,lengths[type][size] - depths[type][size]])
children();
%for (r = [0,180]) {
rotate(r)
translate([0, -d/2, h/2])
rotate([90,90,0])
linear_extrude(1)
text(labels[type], halign="center", valign="center", size=10);
}
}
// Now we get into the production buy-at-the-store components.
// The default size is 3/4", the default type of connection is PVC
// slip, and the default is schedule 40. (The schedule is used
// only for the BOM, though in the future it might be used to model
// wall thickness.)
// This is a straight-through connector. Because you can specify the
// type of each end, you can model slip-slip, slip-MIPT, et cetera.
// Size is the size of the part. It should probably be split into
// size1 and size2 to allow for size-conversion connectors.
// Type1 is the type of the "previous" connection; type2 is the
// type of the "next" connection.
module connector(size=S34, type1=SLIP, type2=SLIP, sched=40) {
echo("connector", labels[type1], labels[type2], slabels[size], str("sch", sched));
translate([0,0,-depths[type1][size]]) {
end(size=size, type=type1);
h = lengths[type1][size];
translate([0,0,h]) {
end(size=size, type=type2)
children();
}
}
}
// A 45 degree angle. Note that I didn't bother to model the actual
// joint in the middle; I should throw a sphere in there.
module angle45(size=S34, type1=SLIP, type2=SLIP, sched=40) {
echo("angle45", labels[type1], labels[type2], slabels[size], str("sch", sched));
translate([0,0,-depths[type1][size]]) {
end(size=size, type=type1);
h = lengths[type1][size];
translate([0,0,h]) {
rotate([0,45,0]) end(size=size, type=type2)
children();
}
}
}
// A 90 degree elbow.
// size controls the size of the part (and should be split into size1
// and size2).
// type1 controls the "previous" type; type2 controls the "next" type.
module elbow(size=S34, type1=SLIP, type2=SLIP, sched=40) {
echo("elbow", labels[type1], labels[type2], slabels[size], str("sch", sched));
translate([0,0,-depths[type1][size]]) {
end(size=size, type=type1);
translate([0,0,lengths[type1][size]]) {
cylinder(h=ods[SLIP][size]/2, d=ods[SLIP][size]);
translate([0,0,ods[type2][size]/2]) {
sphere(d=ods[SLIP][size]);
rotate([0,90,0]) {
cylinder(h=ods[SLIP][size]/2, d=ods[SLIP][size]);
translate([0,0,ods[type1][size]/2]) {
end(size=size, type=type2)
children();
}
}
}
}
}
}
// A three-way corner. size1 and type1 are for the "previous"
// connection. If you imagine looking into the part horizontally,
// with one of the other two connectors pointing left and the other
// pointing down, size2 and type2 point left and size3 and type3
// point down.
module corner(size1=S34, size2=S34, size3=S34, type1=SLIP, type2=SLIP, type3=SLIP, sched=40) {
if (size1 != size2 || size2 != size3 || type1 != type2 || type2 != type3) {
echo("WARNING: corner() does not align mixed-type parts correctly.");
}
echo("corner", labels[type1], labels[type2], labels[type3], slabels[size1], slabels[size2], slabels[size3], str("sch", sched));
translate([0,0,-depths[type1][size1]]) {
end(size=size1, type=type1);
translate([0,0,lengths[type1][size1]]) {
cylinder(h=ods[SLIP][size1]/2, d=ods[SLIP][size1]);
translate([0,0,ods[type1][size1]/2]) {
rotate([0,90,0]) {
cylinder(h=ods[SLIP][size1]/2, d=ods[SLIP][size1]);
translate([0,0,ods[SLIP][size2]/2])
end(size=size2, type=type2)
if ($children > 1) children(0);
}
rotate([90,0,0]) {
cylinder(h=ods[SLIP][size3]/2, d=ods[SLIP][size3]);
translate([0,0,ods[SLIP][size3]/2])
end(size=size3, type=type3)
if ($children > 0) children(1);
}
}
}
}
}
// A tee connector, approached along the top of the T.
// size and type control the body, and size3 and type3 control the
// body of the T. Probably size and type should split into size1/2 and
// type1/2, though real-world connectors don't have that many options.
module tee(size=S34, size3=S34, type=SLIP, type3=SLIP, sched=40) {
echo("tee", labels[type], labels[type3], slabels[size], slabels[size3], str("sch", sched));
translate([0,0,-depths[type][size]]) {
end(size=size, type=type);
translate([0,0,depths[type][size]]) {
translate([0,0,ods[type3/2][size]/2]) {
rotate([0,90,0]) cylinder(h=ods[SLIP][size]/2, d=ods[SLIP][size]);
translate([ods[SLIP][size]/2,0,0])
rotate([0,90,0])
end(size=size3, type=type3)
if ($children > 1) children(1);
}
cylinder(h=ods[type3][size3], d=ods[SLIP][size]);
translate([0,0,ods[type3][size3]])
end(size=size, type=type)
if ($children > 0) children(0);
}
}
}
// A tee, approached from the body of the tee.
// As for tee(), size3 and type3 refer to the body of the T,
// even though here that's the direction we're approaching from.
module tee2(size=S34, size3=S34, type=SLIP, type3=SLIP, sched=40) {
echo("tee", labels[type], labels[type3], slabels[size], slabels[size3], str("sch", sched));
translate([0,0,-depths[type3][size3]]) {
end(size=size3, type=type3);
translate([0,0,lengths[type3][size3]]) {
cylinder(h=ods[SLIP][size]/2, d=ods[SLIP][size]);
translate([0,0,ods[type][size]/2]) {
rotate([0,90,0]) cylinder(h=ods[SLIP][size], d=ods[SLIP][size], center=true);
rotate([0,90,0])
translate([0,0,ods[SLIP][size]/2])
end(size=size, type=type)
if ($children > 1) children(1);
rotate([0,-90,0])
translate([0,0,ods[SLIP][size]/2])
end(size=size, type=type)
if ($children > 0) children(0);
}
}
}
}
// A hose bib. Very approximate.
module hosebib(size=S34, type=MIPT) {
echo("hosebib", labels[type], slabels[size]);
translate([0,0,depths[type][size]]) {
end(size=size, type=type);
translate([0,0,lengths[type][size]]) {
cylinder(h=20, d=hosebib_ods[size]);
translate([0,0,20]) {
rotate([30,0,0]) cylinder(h=33, d=26);
rotate([-60,0,0]) cylinder(h=45, d=25);
translate([0,40,25]) rotate([0,90,0]) cylinder(h=60,d=11,center=true);
}
}
}
}
// A pipe.
// You can either use a size index or you can directly specify
// a diameter. Directly specifying a diameter was a hackish way
// to create a generic unmodeled part. You can specify a label,
// for generic unmodeled parts.
module pipe(size=S34, d, length, label) {
d = d == undef ? ods[PIPE][size] : d;
cylinder(h=length, d=d);
if (label != undef) {
%for (a = [0,180]) {
rotate(a)
translate([0, -d/2, length/2])
rotate([90,90,0])
linear_extrude(1)
text(label, halign="center", valign="center", size=10);
}
}
translate([0,0,length]) children();
}
// A function to abstract a couple of externally-important
// dimensions of a sprinkler valve.
function sprinkler_valve_dims(size=S34) =
let(dims=spr_valve_dims[size])
[
dims[16], // c-to-c
dims[17], // union_h2
dims[20] // sep
];
// A Champion valve body and compact automatic actuator.
// I don't know whether the input and output can be different connector
// types; the ones I have are both FIPT.
// Note that this is a connector; you connect to its input side and
// the next component is attached to its output side.
module sprinkler_valve(size=S34, type=FIPT) {
dims = spr_valve_dims[size];
ball_d = dims[0];
inlet_od = dims[1];
body = dims[2];
union_od = dims[3];
union_h = dims[4];
cap_d = dims[5];
cap_h = dims[6];
neck_h = dims[7];
neck_d = dims[8];
saucer_h = dims[9];
saucer_d = dims[10];
screw_h = dims[11];
screw_d = dims[12];
sol_x = dims[13];
sol_h = dims[14];
sol_d = dims[15];
c_to_c = dims[16];
union_h2 = dims[17];
union_d2 = dims[18]; //32
cap_x = dims[19];
sep = dims[20];
body_z = 15;
neck_z = body_z + body.z;
translate([0,0,-depths[type][size]]) {
// Should use end().
cylinder(h=30, d=inlet_od, $fn=6);
translate([0,0,10 + ball_d/2]) sphere(d=ball_d);
translate([0,0,neck_z - 30]) cylinder(h=30, d=inlet_od);
translate([0,-body.y/2,body_z]) cube([c_to_c+body.y/2, body.y, body.z]);
out_center_x = body.x-ball_d/2-body.y/2;
translate([c_to_c,0,0]) cylinder(h=union_h,d=union_od);
translate([c_to_c,0,0]) rotate([180,0,0]) {
// Should use end().
cylinder(h=union_h2, d=union_d2);
translate([0,0,union_h2-depths[type][size]])
children();
}
translate([cap_x,0,body_z+body.z]) cylinder(h=cap_h, d=cap_d, $fn=6);
translate([0,0,neck_z]) cylinder(h=neck_h, d=neck_d, $fn=6);
saucer_z = neck_z + neck_h;
translate([0,0,saucer_z]) cylinder(h=saucer_h, d=saucer_d);
screw_z = saucer_z + saucer_h;
translate([0,0,screw_z]) cylinder(h=screw_h, d=screw_d);
translate([sol_x,0,screw_z]) cylinder(h=sol_h, d=sol_d);
}
}
// A not-very-completely-modeled pressure regulator.
// Connectors not really modeled right.
function reg_dims(size=S34) = [[ 134,70,136 ], 30];
module regulator(size=S34, type=FIPT) {
dims = reg_dims();
box = dims[0];
c_z = dims[1];
translate([0,0,-depths[type][size]]) {
translate([-c_z,-box.y/2,0]) {
cube([box.z,box.y,box.x]);
}
translate([0,0,box.x-depths[type][size]])
children();
}
}
//
// And now on to the actual plumbing project.
//
// Some dimensions: how high I want the valves off
// the ground, how deep I want the bottom of the manifold
// buried, how high the regulator should be, separation
// between valves.
//
// +X is east
// +Y is north
// Eventually I want to have an exploded view. But not yet.
explode = false;
valve_z = 280;
manifold_depth = 200;
reg_z = valve_z;
sv_sep = sprinkler_valve_dims()[2];
// Variation 1: with pressure regulator.
// I didn't end up using this variation.
module plan1() {
pipe(length=reg_z-50)
connector()
pipe(length=50)
rotate(180) elbow(type1=SLIP, type2=MIPT, sched=80)
rotate(180) regulator()
rotate(180) elbow(type1=MIPT, type2=SLIP)
pipe(length=50)
rotate(180) tee(size3=S34, type3=FIPT) {
pipe(length=manifold_depth + reg_z - 80)
rotate(-90) elbow()
pipe(length=sv_sep)
tee() {
pipe(length=sv_sep)
elbow()
pipe(length=valve_z + manifold_depth)
rotate(-90) sprinkler_valve_assy()
pipe(length=valve_z);
pipe(length=valve_z + manifold_depth)
rotate(-90) sprinkler_valve_assy()
pipe(length=valve_z);
}
rotate(-90) hosebib();
}
}
// Variation 2: Starting downstream of existing regulator,
// at a different point in the system.
// This is what I ended up building.
module plan2() {
rotate([180,0,0])
connector(type1=MIPT, size=S1)
pipe(length=50, size=S1)
rotate(90) elbow(size=S1)
pipe(length=100, size=S1)
corner(size1=S1,size2=S1) {
pipe(length=400, size=S1)
rotate(180) sprinkler_valve_assy(size=S1);
pipe(length=sv_sep)
tee() {
pipe(length=sv_sep)
elbow()
pipe(length=300)
rotate(180) tee(type3=FIPT) {
pipe(length=70)
rotate(-90) sprinkler_valve_assy()
pipe(length=400)
rotate(-90) elbow()
pipe(length=1450)
rotate(90) angle45()
pipe(length=1000);
rotate(90) hosebib();
}
pipe(length=400)
rotate(90) sprinkler_valve_assy()
pipe(length=430)
rotate(-90) elbow()
pipe(length=1500)
rotate(90) angle45()
pipe(length=1000);
}
};
}
plan2();
// A sprinkler valve assembly, with the two PVC connectors attached.
// Note that this is a connector; the input side connects to the
// previous component and the child is the output side.
module sprinkler_valve_assy(size=S34) {
connector(type1=SLIP, type2=MIPT, size=size)
sprinkler_valve(size=size)
connector(type1=MIPT, type2=SLIP, size=size)
children();
}