// ============================================================================= // Woven planter + drip tray with bayonet (twist) lock // Parametric. // Reverse-engineered from "Planter - 100mm" and "Drip Tray - 100mm" STLs (https://www.printables.com/model/1507840-weave-planter). // // Outside: diagonal basket weave (two strip directions crossing over/under, // 45 deg chamfered strip edges, dark diamond gaps between strips). // Lock: 4 lugs on the tray rim pass between 4 stops on the underside of // the planter. Push the planter down onto the tray with the lugs in // the gaps, then twist ~45 deg so every lug sits over a stop. // // Both parts print as modelled (flat side down, no supports): // - weave strips have 45 deg chamfers, ends of the weave taper 45 deg // - lugs and stops have 45 deg flanks // - the only bridge is the 5 mm slot ceiling inside the planter floor // ============================================================================= /* [Render] */ part = "both"; // [both, planter, tray, assembled] /* [Main dimensions] */ outer_d = 93.6; // max outer diameter, mm (rim bead / tray foot) planter_h = 79.92; // planter height, mm tray_h = 26.4; // tray height incl. lock rim, mm /* [Walls & floor] */ wall_t = 3.2; // planter wall thickness at the raised weave floor_center = 2.45; // planter floor thickness at the centre floor_edge = 8.0; // planter floor height at the wall (cone slopes ~7 deg) tray_floor_center = 2.4; // tray floor thickness at the centre tray_floor_edge = 8.0; // tray floor height at the rim (water runs to the centre) /* [Drainage] */ drain_d = 4.0; // drain hole diameter (keep >= 3.5 mm for PETG) hole_pitch = 15; // radial spacing between hole rings holes_per_ring = 8; // holes on every ring (+1 in the centre) hole_margin = 4; // keep holes this far from the plug edge /* [Weave] */ texture = true; // weave on/off (off = smooth walls) weave_pitch = 10.2; // strip pitch (centre to centre), mm; snapped so it fits the circumference relief = 0.9; // weave depth, mm weave_zphase = 1.4; // vertical start phase of the weave, mm weave_rot = 8.5; // rotate the weave pattern around the part, degrees /* [Bayonet lock] */ lug_count = 4; // number of lugs on the tray / stops on the planter lug_w = 24; // angular width of each lug and each stop, degrees lug_depth = 2.2; // how far the lugs reach inwards, mm lug_z0 = 1.8; // height of the lug start above the tray shoulder, mm rim_t = 2.2; // tray rim wall thickness rim_h = 6.38; // tray rim height above the shoulder fit_c = 0.25; // radial clearance rim <-> planter wall (raise to 0.3 if tight) lug_c = 0.3; // radial clearance lug tip <-> planter plug lock_c = 0.3; // vertical clearance lug <-> stop (smaller = tighter lock) lug_angle = 0; // tray lug rotation in the assembled view (0 = locked, 45 = unlocked) // ----------------------------------------------------------------- derived --- bead_r = 1.2; bead_h = 2.4; neck_h = 1.2; R = outer_d / 2; // bead radius R_t = R - 0.4; // top of the weave strips Rc = R_t - relief; // base (gap) level of the weave R_in = R_t - wall_t; // planter inner wall radius wall_r = texture ? Rc - 0.3 : R_t; // core wall radius (weave skin is added on top) neck_r = Rc - 0.1; // neck under the bead / above the foot C = 2 * PI * R_t; N = max(6, round(C / (sqrt(2) * weave_pitch))); // diamonds per row around the part cellx = C / N; // horizontal distance between neighbouring diamonds fn_core = 10 * N; // facets of the round parts (rim, bead, lock) // bayonet geometry flank_max = (rim_h - lug_z0 - 0.15) / 2; lug_d = min(lug_depth, flank_max); // lug reach, 45 deg flanks lug_tf = rim_h - lug_z0 - 2 * lug_d; // flat tip height R_rim_o = R_in - fit_c; // tray rim outer radius R_rim_i = R_rim_o - rim_t; // tray rim inner radius tip_r = R_rim_i - lug_d; // lug tip radius plug_r = tip_r - lug_c; // planter floor plug radius blk_r = R_rim_i - 0.25; // stop outer radius blk_z = lug_z0 + (R_rim_i - blk_r) - lock_c; // stop vertical land before the 45 deg top pocket_h = rim_h + 0.3; // slot ceiling inside the planter z_s = tray_h - rim_h; // tray shoulder height fe_p = max(floor_edge, pocket_h + 1.2); planter_skin_z1 = planter_h - bead_h - neck_h; tray_skin_z0 = bead_h + neck_h; assert(lug_tf >= 0.1, "rim_h too small for the lug: raise rim_h or lower lug_z0"); assert(blk_z + (blk_r - plug_r) < pocket_h - 0.5, "slot too shallow for the stops: raise rim_h"); assert(z_s > tray_floor_edge + 6, "tray_h too small: raise tray_h or lower tray_floor_edge"); assert(planter_h > 25, "planter_h too small"); assert(plug_r > 15, "outer_d too small for the lock geometry"); // ------------------------------------------------------------------- weave --- // Plain diagonal weave: two strip families run at +/-45 deg and pass over / under each other in a checker. (u, v) are lattice coordinates, one unit = one strip pitch; u runs up-right, v runs down-right along the wall. strip_half = 0.30; // half width of a strip, in pitches strip_edge = 0.05; // width of the chamfered strip edge, in pitches ramp_floor = 0.25; // height an under-going strip ramps down to (fraction of relief) weave_detail = 4; // mesh facets across each strip top: 1 coarse ... 4 very smooth (3 renders faster) function tri(z) = abs(z - 2 * floor(z / 2) - 1); function Tb(f) = max(0, min(1, (strip_half - abs(f - 0.5)) / strip_edge)); // across a strip function Wv(q) = ramp_floor + (1 - ramp_floor) * max(0, min(1, (tri(q - 0.5) - strip_half) / (1 - 2 * strip_half))); // along a strip function weave_h(u, v) = // surface height 0..1 let (I = floor(u), J = floor(v), a = min(Tb(u - I), Wv(v + I)), b = min(Tb(v - J), Wv(u + J + 1))) max(a, b); // The surface mesh is built on the lattice lines where the weave changes slope (strip edges and ramp ends). Every edge of every strip is therefore a straight mesh edge, and the flat tops, ramps and gaps need only a handful of facets. Extra lines across the flat strip tops and the gaps keep them round (one big facet would chord across the curved wall) and give smoother shading. weave_detail sets how many facets span each strip top: 1 = coarse, 3 = smooth, 4 = very smooth (default). function gap_lines(k0, k1) = [for (k = [k0 : k1]) 0.5 + strip_half + (1 - 2 * strip_half) * k / weave_detail]; low_g = [for (f = gap_lines(1, weave_detail - 1)) if (f >= 1 - 1e-9) f - 1]; // gap, next cell high_g = [for (f = gap_lines(1, weave_detail - 1)) if (f < 1 - 1e-9) f]; // gap, this cell plat = [for (k = [0 : weave_detail]) 0.5 - strip_half + strip_edge + (2 * strip_half - 2 * strip_edge) * k / weave_detail]; Fl = concat(low_g, [0.5 - strip_half], plat, [0.5 + strip_half], high_g); // mesh lines per pitch nl = len(Fl); function mline(a) = floor(a / nl) + Fl[((a % nl) + nl) % nl]; // lattice coordinate of mesh line a function g_y(a, dl) = (mline(a) - mline(a + dl)) * cellx / 2 + weave_zphase; // height on the wall function g_h(a, dl) = weave_h(mline(a), mline(a + dl)); // P = [lines around, rows, first row, last row, y min, y max] function g_vi(a, dl, P) = (((a % P[0]) + P[0]) % P[0]) * P[1] + (dl - P[2]); function g_in(a, dl, P) = dl >= P[2] && dl <= P[3] && g_y(a, dl) >= P[4] && g_y(a, dl) <= P[5]; function g_q(a, dl, P) = g_in(a, dl, P) && g_in(a + 1, dl - 1, P) && g_in(a + 1, dl, P) && g_in(a, dl + 1, P); function g_pt(a, dl, z0, inner) = let (u = mline(a), v = mline(a + dl), th = 180 * (u + v) / N, r = inner ? Rc - 0.8 : Rc + relief * weave_h(u, v)) [r * cos(th), r * sin(th), z0 + (u - v) * cellx / 2 + weave_zphase]; function g_faces(a, dl, P, off) = let (p00 = g_vi(a, dl, P), p10 = g_vi(a + 1, dl - 1, P), p11 = g_vi(a + 1, dl, P), p01 = g_vi(a, dl + 1, P), i00 = p00 + off, i10 = p10 + off, i11 = p11 + off, i01 = p01 + off, d1 = abs(g_h(a, dl) - g_h(a + 1, dl)), d2 = abs(g_h(a + 1, dl - 1) - g_h(a, dl + 1))) concat( // outer surface: split the quad along the diagonal that bends the least d1 <= d2 ? [[p00, p10, p11], [p00, p11, p01]] : [[p00, p10, p01], [p10, p11, p01]], // inner wall (hidden inside the core) [[i00, i01, i11], [i00, i11, i10]], // side walls where the patch ends (these lie outside the part and get trimmed away) g_q(a, dl - 1, P) ? [] : [[p10, p00, i00], [p10, i00, i10]], g_q(a + 1, dl - 1, P) ? [] : [[p11, p10, i10], [p11, i10, i11]], g_q(a, dl + 1, P) ? [] : [[p01, p11, i11], [p01, i11, i01]], g_q(a - 1, dl + 1, P) ? [] : [[p00, p01, i01], [p00, i01, i00]]); // Ring-shaped skin whose outer surface is the weave between z0 and z1, with the // 45 deg run-out at both ends (trimmed by the envelope below). module weave_skin(z0, z1) { L = z1 - z0; M = nl * N; ylo = -0.6 * cellx; yhi = L + 0.6 * cellx; dlo = floor((weave_zphase - yhi) * 2 * nl / cellx) - 3; dhi = ceil((weave_zphase - ylo) * 2 * nl / cellx) + 3; Dn = dhi - dlo + 1; P = [M, Dn, dlo, dhi, ylo, yhi]; off = M * Dn; pts = concat([for (a = [0 : M - 1], dl = [dlo : dhi]) g_pt(a, dl, z0, false)], [for (a = [0 : M - 1], dl = [dlo : dhi]) g_pt(a, dl, z0, true)]); fcs = [for (a = [0 : M - 1], dl = [dlo : dhi]) if (g_q(a, dl, P)) each g_faces(a, dl, P, off)]; Rbig = R_t + 0.6; k = Rbig - Rc; // 45 deg run-out assert(L > 2 * k + 1, "weave zone too short"); intersection() { rotate([0, 0, weave_rot]) polyhedron(points = pts, faces = fcs, convexity = 10); translate([0, 0, z0]) rotate_extrude($fn = fn_core) polygon([[Rc - 1, 0], [Rc, 0], [Rbig, k], [Rbig, L - k], [Rc, L], [Rc - 1, L]]); } } // ----------------------------------------------------------------- planter --- module planter_profile() { difference() { union() { polygon([[0, 0], [wall_r, 0], [wall_r, planter_skin_z1 - 0.2], [neck_r, planter_skin_z1 - 0.2], [neck_r, planter_h], [R_in, planter_h], [R_in, fe_p], [0, floor_center]]); // rim bead: round on top, 45 deg underneath (printable) cx = R - bead_r; cz = planter_h - bead_r; tx = cx + bead_r * cos(45); tz = cz - bead_r * sin(45); zP = tz - (tx - neck_r); hull() { translate([cx, cz]) circle(r = bead_r, $fn = 48); polygon([[R_in, planter_h], [R_in, zP], [neck_r, zP]]); } } // annular slot on the underside that receives the tray rim polygon([[plug_r, -1], [R_in, -1], [R_in, pocket_h], [plug_r, pocket_h]]); } } // stops on the plug: the tray lugs sit on top of these once twisted module lock_stops() { zt = blk_z + (blk_r - plug_r); for (k = [0 : lug_count - 1]) rotate([0, 0, k * 360 / lug_count - lug_w / 2]) rotate_extrude(angle = lug_w, $fn = fn_core) polygon([[plug_r - 0.6, 0], [blk_r, 0], [blk_r, blk_z], [plug_r, zt], [plug_r - 0.6, zt]]); } module drain_holes() { n_rings = floor((plug_r - hole_margin - drain_d / 2) / hole_pitch); translate([0, 0, -1]) cylinder(d = drain_d, h = fe_p + 2, $fn = 32); for (i = [1 : n_rings], k = [0 : holes_per_ring - 1]) rotate([0, 0, k * 360 / holes_per_ring]) translate([i * hole_pitch, 0, -1]) cylinder(d = drain_d, h = fe_p + 2, $fn = 32); } module planter() { union() { difference() { union() { rotate_extrude($fn = fn_core) planter_profile(); lock_stops(); } drain_holes(); } if (texture) weave_skin(0, planter_skin_z1); } } // -------------------------------------------------------------------- tray --- module tray_profile() { union() { polygon([[0, 0], [neck_r, 0], [neck_r, tray_skin_z0 + 0.2], [wall_r, tray_skin_z0 + 0.2], [wall_r, z_s], [R_rim_o, z_s], [R_rim_o, z_s + rim_h], [R_rim_i, z_s + rim_h], [R_rim_i, tray_floor_edge], [0, tray_floor_center]]); // rounded foot bead r = 0.7; hull() { translate([R - r, r]) circle(r = r, $fn = 32); translate([R - r, bead_h - r]) circle(r = r, $fn = 32); square([R - r, bead_h]); } } } // bayonet lugs on the inside of the rim: 45 deg flanks, flat tip module lock_lugs() { z_a = z_s + lug_z0; for (k = [0 : lug_count - 1]) rotate([0, 0, k * 360 / lug_count + lug_angle - lug_w / 2]) rotate_extrude(angle = lug_w, $fn = fn_core) polygon([[R_rim_i + 0.5, z_a], [R_rim_i, z_a], [tip_r, z_a + lug_d], [tip_r, z_a + lug_d + lug_tf], [R_rim_i, z_a + 2 * lug_d + lug_tf], [R_rim_i + 0.5, z_a + 2 * lug_d + lug_tf]]); } module tray() { union() { rotate_extrude($fn = fn_core) tray_profile(); lock_lugs(); if (texture) weave_skin(tray_skin_z0, z_s); } } // ------------------------------------------------------------------ output --- if (part == "planter") planter(); else if (part == "tray") tray(); else if (part == "assembled") { tray(); translate([0, 0, z_s]) planter(); } else { planter(); translate([outer_d + 10, 0, 0]) tray(); }