// ==================================================================== // Two-part self-watering flower pot, designed for PETG // ==================================================================== // OUTER pot : watertight reservoir cup. Spiral ridges on the outside, smooth tapered inside, with a conical seat at the rim. // INNER pot : planter with drainage holes. Its conical underside rests on the seat, which leaves a water reservoir underneath. It lifts straight out (tapered fit, no snap that can jam). // ==================================================================== /* [What to render] */ part = "print"; // [print:Both parts laid out for printing, outer:Outer pot only, inner:Inner pot only, assembly:Assembled preview, section:Assembled cut-away preview] /* [Main dimensions (mm)] */ // Outer diameter at the top, measured over the ridge crests pot_diameter = 100; // Height of the outer pot (rim included) pot_height = 100; // Bottom diameter / top diameter (1 = straight sides, lower = more taper) bottom_ratio = 0.8; /* [Reservoir, walls and fit] */ // Water space between the outer floor and the underside of the inner pot reservoir_depth = 25; // Wall thickness (6 x 0.4 mm lines) wall = 2.4; // Floor thickness of both parts base_thickness = 2.4; // Gap between the inner pot and the outer pot (raise for a looser fit) clearance = 0.4; // Height of the collar that stands above the outer rim (also your finger grip) collar_height = 1; // Horizontal width of the conical seat at the top of the outer pot seat = 1.4; // Steepness of the seat, measured from vertical (smaller = steeper = easier to print; max 50) seat_angle = 40; /* [Spiral pattern] */ // Number of ridges around the pot ridge_count = 12; // Peak-to-valley depth of the ridges ridge_depth = 2.4; // Total twist from bottom to top in degrees (negative = other direction) twist = 180; /* [Drainage holes in the inner pot] */ hole_diameter = 5; // Centre-to-centre distance between holes hole_spacing = 12; // Keep-out distance between holes and the wall hole_margin = 3; /* [Hidden] */ $fn = 1024; bottom_chamfer = 1.2; // height of the elephant-foot chamfer on the outer pot bottom_chamfer_run = 0.8; // its horizontal width (less than the height = under 45 degrees) inner_chamfer = 0.6; // horizontal width of the 1 mm high elephant-foot chamfer on the inner pot // --------------------------------------------------------------------- // Derived geometry // --------------------------------------------------------------------- Rt = pot_diameter / 2; // crest radius at the top Rb = Rt * bottom_ratio; // crest radius at the bottom s_top = Rt / Rb; // scale applied by the twisted extrusion Rv = Rt * (1 - ridge_depth / Rb); // valley radius at the top Cb = Rb - ridge_depth - wall; // cavity radius at z = 0 Ct = Rv - wall; // cavity radius at z = pot_height zb = base_thickness + reservoir_depth; // z of the inner pot's underside m = (Ct - Cb) / pot_height; // slope of the cavity radius (taper) k = tan(seat_angle); // slope of the seat cone (radius per mm of height) seat_dz = seat / k; // vertical height of the seat z_seat = pot_height - seat_dz; // where the seat starts function lerp_z(a, b, z) = a + (b - a) * z / pot_height; function cav(z) = lerp_z(Cb, Ct, z); // outer pot: inner (cavity) radius function rb(z) = cav(z) - clearance; // inner pot: outer radius function env(z) = lerp_z(Rb, Rt, z); // crest radius r_seat_top = cav(z_seat) + seat; // seat radius at the rim height where the inner pot's wall meets the seat cone z1 = z_seat - clearance / (k - m); // --------------------------------------------------------------------- // Sanity checks // --------------------------------------------------------------------- rim_flat = Rv - r_seat_top; overhang = atan((ridge_depth / 2) * ridge_count * (abs(twist) * PI / 180 / pot_height) * s_top + (Rt - Rb) / pot_height); assert(bottom_ratio > 0.4 && bottom_ratio <= 1, "bottom_ratio must be between 0.4 and 1"); assert(pot_height > reservoir_depth + 2 * base_thickness + 30, "pot_height is too small for this reservoir_depth"); assert(seat_angle > atan(m) + 5 && seat_angle <= 50, "seat_angle must be between about 10 and 50 degrees"); assert(rim_flat >= 0.8, "Rim too thin: increase wall, or reduce seat / ridge_depth"); assert(z1 > zb + 2, "Seat is too low for this reservoir_depth: reduce seat or increase pot_height"); assert(Ct > hole_diameter, "pot_diameter is too small for these settings"); echo(str("Outer pot, steepest outward overhang: about ", round(overhang), " deg (PETG prints well up to ~40-45)")); echo(str("Inner pot, steepest overhang: ", seat_angle, " deg (seat cone); no flat underside")); if (overhang > 45) echo("WARNING: ridges too steep. Reduce twist, ridge_depth or ridge_count."); echo(str("Flat rim width: ", rim_flat, " mm")); echo(str("Reservoir volume: about ", round(PI * pow(cav(zb / 2), 2) * reservoir_depth / 1000), " ml")); // --------------------------------------------------------------------- // OUTER pot // --------------------------------------------------------------------- module wavy_profile() { n = ridge_count * 16; polygon([for (i = [0 : n - 1]) let (a = 360 * i / n, r = Rb - ridge_depth / 2 + (ridge_depth / 2) * cos(ridge_count * a)) [r * cos(a), r * sin(a)]]); } module spiral_body() { linear_extrude(height = pot_height, twist = twist, scale = s_top, slices = max(ceil(pot_height / 2), ceil(abs(twist) / 2)), convexity = 10) wavy_profile(); } // Chamfers the bottom edge only; follows the crest line elsewhere. module envelope() { rotate_extrude() polygon([[0, 0], [env(bottom_chamfer) - bottom_chamfer_run + 0.05, 0], [env(bottom_chamfer) + 0.05, bottom_chamfer], [Rt + 0.05, pot_height], [0, pot_height]]); } module outer_cavity() { rotate_extrude() polygon([[0, base_thickness], [cav(base_thickness), base_thickness], [cav(z_seat), z_seat], [r_seat_top + k, pot_height + 1], // conical seat, continued past the rim [0, pot_height + 1]]); } module outer_part() { difference() { intersection() { spiral_body(); envelope(); } outer_cavity(); } } // --------------------------------------------------------------------- // INNER pot // --------------------------------------------------------------------- // The outside widens along the seat cone from the wall (z1) up to the rim, then rises as a vertical collar. Nothing hangs out horizontally. module inner_profile() { polygon([[0, zb], [rb(zb + 1) - inner_chamfer, zb], // bottom chamfer, under 45 degrees [rb(zb + 1), zb + 1], [rb(z1), z1], [r_seat_top, pot_height], // rests on the outer pot's seat [r_seat_top, pot_height + collar_height], [0, pot_height + collar_height]]); } module inner_cavity() { z0 = zb + base_thickness; rotate_extrude() polygon([[0, z0], [rb(z0) - wall, z0], [rb(pot_height) - wall, pot_height], [rb(pot_height) - wall, pot_height + collar_height + 1], [0, pot_height + collar_height + 1]]); } module drain_holes() { fr = rb(zb) - wall - hole_margin; rings = floor((fr - hole_diameter / 2) / hole_spacing); translate([0, 0, zb - 1]) linear_extrude(height = base_thickness + 2) union() { circle(d = hole_diameter, $fn = 24); if (rings >= 1) for (k = [1 : rings]) { r = k * hole_spacing; n = floor(2 * PI * r / hole_spacing); for (i = [0 : n - 1]) rotate(i * 360 / n + (k % 2) * 180 / n) translate([r, 0]) circle(d = hole_diameter, $fn = 24); } } } module inner_part() { difference() { rotate_extrude() inner_profile(); inner_cavity(); drain_holes(); } } // --------------------------------------------------------------------- // Output // --------------------------------------------------------------------- module assembled() { color("sienna") outer_part(); color("seagreen") inner_part(); } if (part == "outer") outer_part(); else if (part == "inner") translate([0, 0, -zb]) inner_part(); // sits on the bed else if (part == "assembly") assembled(); else if (part == "section") difference() { assembled(); translate([-400, -400, -10]) cube([800, 400, pot_height + 100]); } else { // "print" outer_part(); translate([pot_diameter + 10, 0, -zb]) inner_part(); }