diff --git a/seedling_tray.py b/seedling_tray.py index d0402c0..2147d6f 100644 --- a/seedling_tray.py +++ b/seedling_tray.py @@ -20,28 +20,30 @@ def render_scad( rows: int, cell_size: float, tray_height: float, + water_tray_height: float, render_mode: str, ) -> None: - cmd = ( - f'openscad ' - f'-D cols={cols} ' - f'-D rows={rows} ' - f'-D cell_size={cell_size} ' - f'-D tray_height={tray_height} ' - f'-D \'render_mode="{render_mode}"\' ' - f'-o "{out_file}" ' - f'"{scad_file}"' - ) + cmd = [ + "openscad", + "-D", f"cols={cols}", + "-D", f"rows={rows}", + "-D", f"cell_size={cell_size}", + "-D", f"tray_height={tray_height}", + "-D", f"water_tray_height={water_tray_height}", + "-D", f'render_mode="{render_mode}"', + "-o", str(out_file), + str(scad_file), + ] result = subprocess.run( - ["bash", "-lc", cmd], + cmd, cwd=BASE_DIR, text=True, capture_output=True, ) if result.returncode != 0: - print(f"\nFailed to render {scad_file.name}") + print(f"\nFailed to render {scad_file.name} ({render_mode})") if result.stdout: print(result.stdout) if result.stderr: @@ -61,6 +63,7 @@ def main() -> None: rows = ask_int("Number of rows (rows): ") cell_size = ask_float("Inner cell size in mm (cell_size): ") tray_height = ask_float("Tray height in mm (tray_height): ") + water_tray_height = ask_float("Water tray wall height in mm (water_tray_height): ") OUT_DIR.mkdir(parents=True, exist_ok=True) @@ -68,7 +71,7 @@ def main() -> None: if not scad_file.exists(): raise SystemExit(f"Missing file: {scad_file}") - # Render the tray only + # seedling tray render_scad( scad_file, OUT_DIR / "seedlings-tray.stl", @@ -76,10 +79,11 @@ def main() -> None: rows, cell_size, tray_height, + water_tray_height, "tray_only", ) - # Render all plates in a grid + # removable plates render_scad( scad_file, OUT_DIR / "plates.stl", @@ -87,9 +91,34 @@ def main() -> None: rows, cell_size, tray_height, + water_tray_height, "plates_only", ) + # water tray + render_scad( + scad_file, + OUT_DIR / "water-tray.stl", + cols, + rows, + cell_size, + tray_height, + water_tray_height, + "water_tray_only", + ) + + # pressing tool + render_scad( + scad_file, + OUT_DIR / "pusher-tool.stl", + cols, + rows, + cell_size, + tray_height, + water_tray_height, + "tool_only", + ) + print("\nDone.") diff --git a/seedlings_tray/seedlings-tray.scad b/seedlings_tray/seedlings-tray.scad index 12a727f..bf9100c 100644 --- a/seedlings_tray/seedlings-tray.scad +++ b/seedlings_tray/seedlings-tray.scad @@ -1,27 +1,37 @@ // ======================================================== -// PARAMETRIC SEEDLING TRAY & PUSHABLE PLATES +// PARAMETRIC SEEDLING TRAY SYSTEM +// Includes: Tray, Plates, Angled Water Tray, & Ball-Pusher Tool // ======================================================== /* [Grid Configuration] */ -cols = 3; // Number of columns -rows = 2; // Number of rows +cols = 4; // Number of columns +rows = 3; // Number of rows cell_size = 35; // Inner width/length of each square cell (mm) tray_height = 50; // Total height of the tray (mm) /* [Wall & Ledge Dimensions] */ -wall_thickness = 1.6; // Thickness of the walls (mm) +wall_thickness = 1.6; // Thickness of outer and interior walls (mm) bottom_lip = 4.0; // Width of the bottom ledge holding the plate (mm) -floor_thickness = 2.0; // Thickness of the bottom lip (mm) +floor_thickness = 2.0; // Height/thickness of the bottom lip (mm) /* [Pusher Plate Settings] */ -plate_thickness = 2.0; // Thickness of the pushable plate (mm) +plate_thickness = 2.0; // Thickness of the removable plate (mm) clearance = 0.4; // Gap between cell walls and plate for easy sliding (mm) -drain_hole_dia = 4.5; // Diameter of drainage holes in the pushable plate (mm) +drain_hole_dia = 4.5; // Diameter of drainage holes in the plate (mm) + +/* [Angled Water Tray Settings] */ +water_tray_height = 25; // Height of the bottom drip tray (mm) +wt_clearance = 1.5; // Extra room around main tray for easy removal (mm) +wt_flare = 3.5; // Outward wall expansion at top (mm) + +/* [Single Pressing Tool Settings] */ +tool_plate_thick = 3.0; // Thickness of the square pusher plate (mm) +stem_dia = 10.0; // Diameter of the stick (mm) +stem_length = 45.0; // Length of the stick shaft (mm) +handle_ball_dia = 20.0; // Diameter of the handle ball on top (mm) /* [Render Mode] */ -// ["both_layout", "assembled", "tray_only", "plates_only"] -render_mode = "both_layout"; - +render_mode = "all_layout"; // ["all_layout", "assembled", "tray_only", "plates_only", "water_tray_only", "tool_only"] // --- DERIVED CALCULATIONS --- @@ -30,28 +40,61 @@ total_width = cols * pitch + wall_thickness; total_length = rows * pitch + wall_thickness; -// --- MAIN DISPLAY --- +// --- MAIN DISPLAY EXECUTION --- if (render_mode == "tray_only") { seedling_tray(); } else if (render_mode == "plates_only") { plate_grid(); -} else if (render_mode == "both_layout") { +} else if (render_mode == "water_tray_only") { + water_tray(); +} else if (render_mode == "tool_only") { + single_pressing_tool(); +} else if (render_mode == "all_layout") { + // Top Left: Main Tray seedling_tray(); - translate([0, total_length + 10, 0]) + + // Bottom Left: Plates Grid + translate([0, -total_length - 20, 0]) plate_grid(); + + // Top Right: Angled Water Tray + translate([total_width + 25, 0, 0]) + water_tray(); + + // Bottom Right: Single Pressing Tool + translate([total_width + 25 + (total_width / 2), -total_length / 2 - 20, 0]) + single_pressing_tool(); + } else if (render_mode == "assembled") { - // Visual preview - seedling_tray(); - for (r = [0 : rows - 1]) { - for (c = [0 : cols - 1]) { - translate([ - wall_thickness + c * pitch + clearance/2, - wall_thickness + r * pitch + clearance/2, - floor_thickness - ]) - pusher_plate(); + // 1. Water tray at base + color("DarkSlateGray") water_tray(); + + // 2. Seedling tray inside water tray + translate([wall_thickness + wt_clearance/2, wall_thickness + wt_clearance/2, floor_thickness]) { + color("ForestGreen", 0.95) seedling_tray(); + + // Plates on bottom ledges + color("SaddleBrown") { + for (r = [0 : rows - 1]) { + for (c = [0 : cols - 1]) { + translate([ + wall_thickness + c * pitch + clearance/2, + wall_thickness + r * pitch + clearance/2, + floor_thickness + ]) + pusher_plate(); + } + } } } + + // 3. Pressing tool + translate([ + wall_thickness + wt_clearance/2 + wall_thickness + (cell_size / 2), + wall_thickness + wt_clearance/2 + wall_thickness + (cell_size / 2), + -stem_length / 2 + ]) + color("Goldenrod") single_pressing_tool(); } @@ -59,23 +102,19 @@ if (render_mode == "tray_only") { // MODULES // ======================================================== -// Main seedling tray body +// 1. Main seedling tray body module seedling_tray() { difference() { - // Outer solid volume cube([total_width, total_length, tray_height]); - // Cut out each cell pocket and bottom push hole for (r = [0 : rows - 1]) { for (c = [0 : cols - 1]) { x_pos = wall_thickness + c * pitch; y_pos = wall_thickness + r * pitch; - // 1. Main cell compartment translate([x_pos, y_pos, floor_thickness]) cube([cell_size, cell_size, tray_height]); - // 2. Bottom push-through opening translate([x_pos + bottom_lip, y_pos + bottom_lip, -1]) cube([ cell_size - (2 * bottom_lip), @@ -87,16 +126,14 @@ module seedling_tray() { } } -// Single pushable bottom plate +// 2. Single removable bottom plate module pusher_plate() { p_size = cell_size - clearance; offset_margin = p_size / 3.5; difference() { - // Plate body cube([p_size, p_size, plate_thickness]); - // 5-points drainage hole pattern translate([p_size / 2, p_size / 2, -1]) cylinder(h = plate_thickness + 2, d = drain_hole_dia, $fn = 20); @@ -109,7 +146,7 @@ module pusher_plate() { } } -// Plate grid +// 3. Print layout grid matching exact tray cell count module plate_grid() { p_size = cell_size - clearance; spacing = 2; @@ -120,4 +157,39 @@ module plate_grid() { pusher_plate(); } } +} + +// 4. Water Tray with Angled Outward Walls +module water_tray() { + wt_inner_w = total_width + wt_clearance; + wt_inner_l = total_length + wt_clearance; + wt_outer_w = wt_inner_w + (2 * wall_thickness); + wt_outer_l = wt_inner_l + (2 * wall_thickness); + + difference() { + translate([wt_outer_w / 2, wt_outer_l / 2, 0]) + linear_extrude(height = water_tray_height, scale = [(wt_outer_w + 2 * wt_flare) / wt_outer_w, (wt_outer_l + 2 * wt_flare) / wt_outer_l]) + square([wt_outer_w, wt_outer_l], center = true); + + translate([wt_outer_w / 2, wt_outer_l / 2, floor_thickness]) + linear_extrude(height = water_tray_height, scale = [(wt_inner_w + 2 * wt_flare) / wt_inner_w, (wt_inner_l + 2 * wt_flare) / wt_inner_l]) + square([wt_inner_w, wt_inner_l], center = true); + } +} + +// 5. Pressing Tool +module single_pressing_tool() { + p_size = (cell_size - clearance); + + // Square pusher plate at base + translate([0, 0, tool_plate_thick / 2]) + cube([p_size, p_size, tool_plate_thick], center = true); + + // Vertical stick + translate([0, 0, tool_plate_thick]) + cylinder(h = stem_length, d = stem_dia, $fn = 32); + + // Palm ball handle on top + translate([0, 0, tool_plate_thick + stem_length]) + sphere(d = handle_ball_dia, $fn = 40); } \ No newline at end of file