weave planter
This commit is contained in:
1 parent
7620cec17d
commit
bcb20a9a5d
3 files changed
+381
No files matched your search
@@ -12,6 +12,14 @@ To generate the pot and tray STL files:
|
||||
python flower_pot.py
|
||||
```
|
||||
|
||||
### weave_planter
|
||||
|
||||
Parameterized round weave planter with a pot and a tray.
|
||||
To generate the pot and tray STL files:
|
||||
```bash
|
||||
python weave_planter.py
|
||||
```
|
||||
|
||||
### seedlings_tray
|
||||
|
||||
Parameterized seedlings tray with pushable plates.
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
// =============================================================================
|
||||
// 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();
|
||||
}
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/bin/python3
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from utils import ask_float
|
||||
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
SCAD_DIR = BASE_DIR / "flower_pot"
|
||||
OUT_DIR = BASE_DIR / "stl"
|
||||
|
||||
|
||||
def render_scad(
|
||||
scad_file: Path,
|
||||
out_file: Path,
|
||||
outer_d: float,
|
||||
planter_h: float,
|
||||
) -> None:
|
||||
cmd = [
|
||||
"openscad",
|
||||
"-D",
|
||||
f"outer_d={outer_d}",
|
||||
"-D",
|
||||
f"planter_h={planter_h}",
|
||||
"-D",
|
||||
'part="both"',
|
||||
"-o",
|
||||
str(out_file),
|
||||
str(scad_file),
|
||||
]
|
||||
|
||||
print("\nRendering:")
|
||||
print(" ".join(f'"{arg}"' if " " in arg else arg for arg in cmd))
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=BASE_DIR,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"\nFailed to render {scad_file.name}")
|
||||
|
||||
if result.stdout:
|
||||
print(result.stdout)
|
||||
|
||||
if result.stderr:
|
||||
print(result.stderr, file=sys.stderr)
|
||||
|
||||
raise SystemExit(result.returncode)
|
||||
|
||||
print(f"Generated {out_file}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if shutil.which("openscad") is None:
|
||||
raise SystemExit(
|
||||
"OpenSCAD was not found in PATH. "
|
||||
"Install OpenSCAD or make sure the 'openscad' command is available."
|
||||
)
|
||||
|
||||
scad_file = SCAD_DIR / "weave_planter.scad"
|
||||
|
||||
if not scad_file.exists():
|
||||
raise SystemExit(f"Missing SCAD file: {scad_file}")
|
||||
|
||||
print("Weave planter generator")
|
||||
print("The SCAD model has additional geometric constraints, so very small dimensions may fail OpenSCAD's assertions.\n")
|
||||
|
||||
outer_d = ask_float("Outer diameter (mm): ")
|
||||
planter_h = ask_float("Planter height (mm): ")
|
||||
|
||||
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
diameter_str = f"{outer_d:g}"
|
||||
height_str = f"{planter_h:g}"
|
||||
|
||||
out_file = OUT_DIR / f"weave_planter_{diameter_str}x{height_str}mm.stl"
|
||||
|
||||
render_scad(
|
||||
scad_file,
|
||||
out_file,
|
||||
outer_d,
|
||||
planter_h,
|
||||
)
|
||||
|
||||
print("\nDone.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user