// Xenospora: a seed pod from another planet. Twelve flat crescent fins stand // around a crossed core between a base and a crown. The fins only look // twisted: each one is flat, set at a tangent to the circle. /* [Specimen] */ // Height between base and crown height = 220; // [210:5:250] // How far the crescents bow out fullness = 60; // [54:2:66] // All parts, one group at a time, or two pieces to test the slot fit output = "all"; // [all:All parts, fins_a:Fins 1 to 4, fins_b:Fins 5 to 8, fins_c:Fins 9 to 12, structure:Base crown and core, fit:Fit test] // Scored veins and assembly numbers score_detail = true; /* [Material] */ // Sheet thickness $thickness = 3; // [2.8:0.1:3.2] // Added to the slot width clearance = 0.15; // [0.1:0.05:0.3] // Beam width $kerf = 0.15; // [0:0.01:0.25] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 6; $fn = 144; t = $thickness; s = height / 220; R = 30 * s; sw = t + clearance; fin_tab = 8; core_tab = 12; // Centre line and width of a fin, and width of the core, at height v (0 to 1) function cc(v) = s * (6 + 12 * v + fullness * pow(sin(180 * v), 1.3)); function ww(v) = s * (12 + 14 * pow(sin(180 * v), 1.2)); function cp(v) = [cc(v), height * v]; function polar(r, a) = [r * cos(a), r * sin(a)]; function core_w(v) = s * (8 + 13 * pow(sin(180 * clamp((v - 0.18) / 0.64, 0, 1)), 1.4)); module fin_blank() polygon(concat( [for (j = [0:160]) let (v = j / 160) [cc(v) - ww(v) / 2, height * v]], [for (j = [160:-1:0]) let (v = j / 160) [cc(v) + ww(v) / 2, height * v]] )); function pore_normal(v) = let ( dx = s * (12 + fullness * 1.3 * PI * pow(sin(180 * v), 0.3) * cos(180 * v)), dy = height, n = sqrt(dx * dx + dy * dy) ) [-dy / n, dx / n]; // A slot that follows the centre line of the fin from a to b, with round ends module pore(a, b, w) { r = w * s / 2; na = pore_normal(a); nb = pore_normal(b); aa = atan2(na[1], na[0]); ab = atan2(nb[1], nb[0]); polygon(concat( [for (j = [0:72]) let (v = a + (b - a) * j / 72) cp(v) + pore_normal(v) * r], [for (j = [1:36]) cp(b) + polar(r, ab - j * 180 / 36)], [for (j = [1:72]) let (v = b - (b - a) * j / 72) cp(v) - pore_normal(v) * r], [for (j = [1:36]) cp(a) + polar(r, aa - 180 - j * 180 / 36)] )); } module fin(i) { difference() { union() { fin_blank(); translate([6 * s - fin_tab / 2, -t]) rect([fin_tab, t + 0.3]); translate([18 * s - fin_tab / 2, height - 0.3]) rect([fin_tab, t + 0.3]); } pore(0.16, 0.31, 4); pore(0.37, 0.58, 6); pore(0.64, 0.82, 4); } if (score_detail) score() { for (side = [-1, 1]) polyline([for (j = [6:154]) let (v = j / 160) [cc(v) + side * (ww(v) / 2 - 2.3 * s), height * v]]); for (v = [0.2:0.06:0.8]) line([cc(v) + 4 * s, height * v], [cc(v + 0.014) + ww(v + 0.014) / 2 - 2.3 * s, height * (v + 0.014)]); translate([cc(0.055), height * 0.055]) text(str(i), size = 3.5 * s, halign = "center", valign = "center"); translate([cc(0.95), height * 0.95]) text(str(i), size = 3 * s, halign = "center", valign = "center"); } } module cross_socket() { rect([core_tab + clearance, sw], center = true); rect([sw, core_tab + clearance], center = true); } module sockets(top = false) { for (i = [0:11]) rotate(i * 30) translate([R, top ? 18 * s : 6 * s]) rect([sw, fin_tab + clearance], center = true); } // The base (root) and the crown module disc(top = false) { difference() { circle(r = (top ? 43 : 60) * s); sockets(top); cross_socket(); } if (score_detail) score() { outline() circle(r = (top ? 40 : 57) * s); for (i = [0:11]) rotate(i * 30) { translate([top ? 33 * s : 40 * s, top ? 18 * s : 6 * s]) text(str(i + 1), size = 2.8 * s, halign = "center", valign = "center"); line([R - 5 * s, top ? 18 * s : 6 * s], [R - 8 * s, top ? 18 * s : 6 * s]); } translate([0, -15 * s]) text(top ? "CROWN" : "ROOT", size = 3 * s, halign = "center", valign = "center"); if (!top) { translate([0, -48 * s]) text("X E N O S P O R A", size = 4 * s, halign = "center", valign = "center"); translate([0, -54 * s]) text("SPECIMEN 03 / EXOBOTANICA", size = 2.3 * s, halign = "center", valign = "center"); // Core A lines up with this mark line([-18 * s, 0], [-10 * s, 0]); translate([-21 * s, 0]) text("A", size = 3 * s, halign = "center", valign = "center"); } } } module core_blank() polygon(concat( [for (j = [0:160]) let (v = j / 160) [-core_w(v), height * v]], [for (j = [160:-1:0]) let (v = j / 160) [core_w(v), height * v]] )); // Two core halves that cross: A is slotted from the top, B from the bottom module core(a = true) { difference() { union() { core_blank(); translate([-core_tab / 2, -t]) rect([core_tab, t + 0.3]); translate([-core_tab / 2, height - 0.3]) rect([core_tab, t + 0.3]); } translate([-sw / 2, a ? height / 2 : -t - 1]) rect([sw, height / 2 + t + 1]); for (side = [-1, 1], v = [0.36, 0.48, 0.60]) translate([side * (core_w(v) - 6 * s), height * v]) ellipse([2.2 * s, 5 * s]); } if (score_detail) score() { for (side = [-1, 1]) polyline([for (j = [35:125]) let (v = j / 160) [side * (core_w(v) - 2 * s), height * v]]); translate([-4.6 * s, height * 0.1]) text(a ? "A" : "B", size = 3 * s, halign = "center", valign = "center"); translate([-4.6 * s, height * 0.9]) text(a ? "A" : "B", size = 3 * s, halign = "center", valign = "center"); for (side = [-1, 1], v = [0.29:0.06:0.71]) line([side * 3 * s, height * v], [side * (core_w(v + 0.025) - 2 * s), height * (v + 0.025)]); } } module fit_test() { part("Fit test, sockets") { difference() { rect([66, 32], r = 2); translate([17, 16]) rect([sw, fin_tab + clearance], center = true); translate([45, 16]) cross_socket(); } if (score_detail) score() { translate([17, 5]) text("FIN", size = 3, halign = "center"); translate([45, 5]) text("CORE", size = 3, halign = "center"); } } for (a = [true, false]) part(a ? "Fit test, A" : "Fit test, B") { difference() { union() { rect([24, 32]); translate([6, 32]) rect([12, t]); translate([8, -t]) rect([8, t + 0.2]); } translate([12 - sw / 2, a ? 16 : -t - 1]) rect([sw, a ? 20 + t : 17 + t]); } if (score_detail) score() translate([4, 25]) text(a ? "A" : "B", size = 3); } } if (output == "fit") fit_test(); // Assembled: the two cores cross in the root, the twelve fins stand round // them every 30 degrees, and the crown caps them function fin_at(i) = concat([(R - t / 2) * cos(30 * (i - 1)), (R - t / 2) * sin(30 * (i - 1))], [0]); module placed_fin(i) step("Set the twelve fins into their numbered sockets, all bowing the same way") part(str("Fin ", i), at = fin_at(i), rotate = [90, 0, 90 + 30 * (i - 1)]) fin(i); // Steps are numbered in the order they first run, so name them here in build order step("Cross the two cores and glue them into the root") {} step("Set the twelve fins into their numbered sockets, all bowing the same way") {} step("Lower the crown over the top tabs and glue it") {} if (output == "all" || output == "fins_a") for (i = [1:4]) placed_fin(i); if (output == "all" || output == "fins_b") for (i = [5:8]) placed_fin(i); if (output == "all" || output == "fins_c") for (i = [9:12]) placed_fin(i); if (output == "all" || output == "structure") { step("Cross the two cores and glue them into the root") part("Root", at = [0, 0, -t]) disc(false); step("Lower the crown over the top tabs and glue it") part("Crown", at = [0, 0, height]) disc(true); step("Cross the two cores and glue them into the root") { part("Core A", at = [0, t / 2, 0], rotate = [90, 0, 0]) core(true); part("Core B", at = [-t / 2, 0, 0], rotate = [90, 0, 90]) core(false); } } // Assembly: // 1. Slide cores A and B together at right angles and glue them into the // cross socket in the root, with core A on the scored A mark. // 2. Set fins 1 to 12 into the numbered sockets, all bowing the same way. // 3. Lower the crown over the top tabs, numbers matching the root, and glue. // Paint the core a contrasting colour after cutting if you like, keeping // the joints bare.