// Hadal: the skeleton of an imagined deep-sea ray on a display stand. Nine // ribs slot onto a spine, which sits in two cradles on a plinth. The ribs // lean more along the body, so the flat pieces look like a turning animal. /* [Specimen] */ // Length from nose to tail length = 270; // [250:10:300] // The sculpture, or two pieces to test the slot fit first output = "sculpture"; // [sculpture:Sculpture, fit:Fit test] // Scored veins, rib numbers and plinth markings score_detail = true; /* [Material] */ // Sheet thickness $thickness = 3; // [2.5:0.1:4] // Added to the slot width clearance = 0.15; // [0.05:0.05:0.25] // Beam width $kerf = 0.15; // [0:0.01:0.3] /* [Hidden] */ $material = "plywood"; $sheet = [600, 400]; $gap = 6; $fn = 96; s = length / 270; t = $thickness; sw = t + clearance; // Per rib: position on the spine, slot depth, half width, height and lean xs = [35, 55, 75, 95, 115, 135, 155, 175, 195]; zs = [75, 79, 83, 85, 86, 85, 82, 78, 73]; ws = [29, 49, 74, 94, 88, 69, 49, 32, 20]; hs = [20, 27, 32, 34, 31, 27, 23, 20, 17]; leans = [-0.22, -0.17, -0.09, 0, 0.12, 0.21, 0.27, 0.30, 0.30]; cradle_x = [65, 165]; cradle_z = [74, 72]; module rib_envelope(i) multmatrix([[1, 0, 0], [leans[i], 1, 0]]) scale([ws[i] * s, hs[i] * s]) path("M 0 1 C 0.42 1 0.65 0.55 1 0.25 Q 1.07 0.11 0.94 -0.04 C 0.55 -0.28 0.38 -0.67 0 -0.65 C -0.38 -0.67 -0.55 -0.28 -0.94 -0.04 Q -1.07 0.11 -1 0.25 C -0.65 0.55 -0.42 1 0 1 Z"); module rib_bone(i) difference() { intersection() { rib_envelope(i); union() { if (i == 8) rib_envelope(i); else difference() { rib_envelope(i); offset(delta = -6 * s) rib_envelope(i); } translate([-7 * s, -100 * s]) rect([14 * s, 200 * s]); // Cross braces run past the shell before clipping, so no thin tips are left if (ws[i] >= 45) for (side = [-1, 1]) stroke(5 * s) line([side * 4 * s, 0], [side * ws[i] * 1.10 * s, (hs[i] * 0.12 + leans[i] * side * ws[i] * 1.10) * s]); } } // Slot from below, onto the spine translate([-sw / 2, -200 * s]) rect([sw, 200 * s]); } module rib(i) { rib_bone(i); if (score_detail) score() { outline() offset(delta = -3 * s) rib_envelope(i); for (side = [-1, 1]) line([side * 9 * s, 0], [side * ws[i] * 0.97 * s, (hs[i] * 0.105 + leans[i] * side * ws[i] * 0.97) * s]); translate([0, hs[i] * 0.62 * s]) text(str(i + 1), size = 3.5 * s, halign = "center", valign = "center"); } } module spine_envelope() scale(s) path("M 0 77 Q 7 89 16 93 L 13 111 Q 13 118 18 113 L 30 91 C 77 103 137 106 195 87 C 218 84 238 104 263 124 Q 270 128 270 122 C 253 97 232 73 210 63 Q 200 59 190 59 C 136 78 72 75 31 60 Q 15 53 6 64 Q 0 68 0 77 Z"); module spine() { difference() { spine_envelope(); // Rib slots from above, cradle slots from below for (i = [0:8]) translate([xs[i] * s - sw / 2, zs[i] * s]) rect([sw, 100 * s]); for (j = [0:1]) translate([cradle_x[j] * s - sw / 2, -10 * s]) rect([sw, (cradle_z[j] + 10) * s]); translate([15 * s, 76 * s]) ellipse([5.5 * s, 4 * s]); scale(s) path("M 215 74 Q 236 84 253 108 Q 233 91 215 81 Z"); } if (score_detail) score() scale(s) { path("M 6 78 Q 12 88 23 86"); path("M 20 66 C 70 82 139 85 188 68"); path("M 213 69 Q 238 81 260 115"); for (j = [0:1]) translate([cradle_x[j] - 7, cradle_z[j] + 9]) text(j == 0 ? "A" : "B", size = 3, halign = "center"); for (i = [0:8]) translate([xs[i] + 5, zs[i] - 7]) text(str(i + 1), size = 2.7, halign = "center"); } } module cradle_envelope(j) { h = cradle_z[j] * s; union() { hull() { translate([0, h - 3 * s]) circle(r = 9 * s); translate([0, 48 * s]) circle(r = 8 * s); } for (side = [-1, 1]) hull() { translate([0, 52 * s]) circle(r = 8 * s); translate([side * 24 * s, 5 * s]) circle(r = 8 * s); } for (side = [-1, 1]) translate([side * 24 * s - 8 * s, 0]) rect([16 * s, 10 * s]); } } module cradle(j) { difference() { union() { intersection() { cradle_envelope(j); translate([-45 * s, 0]) rect([90 * s, 120 * s]); } // Tabs into the plinth for (side = [-1, 1]) translate([side * 24 * s - 6 * s, -t]) rect([12 * s, t + 0.5]); } translate([-sw / 2, cradle_z[j] * s]) rect([sw, 100 * s]); for (side = [-1, 1]) hull() { translate([side * 22 * s, 12 * s]) circle(r = 2.2 * s); translate([side * 8 * s, 40 * s]) circle(r = 2.2 * s); } } if (score_detail) score() { translate([0, 61 * s]) text(j == 0 ? "A" : "B", size = 4 * s, halign = "center", valign = "center"); for (side = [-1, 1]) polyline([[side * 27 * s, 7 * s], [side * 18 * s, 28 * s], [side * 4 * s, 51 * s]]); } } module plinth() { difference() { scale(s) polygon([[20, -35], [38, -53], [192, -53], [210, -35], [210, 35], [192, 53], [38, 53], [20, 35]]); for (j = [0:1], side = [-1, 1]) translate([cradle_x[j] * s, side * 24 * s]) rect([sw, 12 * s + clearance], center = true); } if (score_detail) score() scale(s) { outline() polygon([[25, -33], [40, -48], [190, -48], [205, -33], [205, 33], [190, 48], [40, 48], [25, 33]]); translate([115, -32]) text("H A D A L", size = 7, halign = "center", valign = "center"); translate([115, -43]) text("SPECIMEN 09 / ABYSSAL ARCHIVE", size = 2.6, halign = "center", valign = "center"); for (x = [75:5:155]) line([x, 39], [x, x % 10 == 0 ? 44 : 42]); for (j = [0:1]) translate([cradle_x[j], 0]) text(j == 0 ? "A" : "B", size = 3.5, halign = "center"); } } module fit_test() { for (j = [0:1]) part(j == 0 ? "Fit test, lower" : "Fit test, upper") { difference() { rect([35, 30], r = 2); translate([17.5 - sw / 2, j == 0 ? -1 : 15]) rect([sw, 16]); } if (score_detail) score() translate([7, 12]) text(j == 0 ? "L" : "U", size = 4); } } if (output == "fit") fit_test(); // Assembled: the spine stands in the two cradles on the plinth, the ribs // across it in their slots across = [90, 0, 90]; if (output == "sculpture") { // The assembly steps, listed first so they number in build order step("Glue cradles A and B into the plinth"); step("Lower the spine into the cradles"); step("Drop ribs 1 to 9 into the numbered slots on top of the spine"); step("Lower the spine into the cradles") part("Spine", at = [0, t / 2, 0], rotate = [90, 0, 0]) spine(); step("Drop ribs 1 to 9 into the numbered slots on top of the spine") for (i = [0:8]) part(str("Rib ", i + 1), at = [xs[i] * s - t / 2, 0, zs[i] * s], rotate = across) rib(i); step("Glue cradles A and B into the plinth") { part("Cradle A", at = [cradle_x[0] * s - t / 2, 0, 0], rotate = across) cradle(0); part("Cradle B", at = [cradle_x[1] * s - t / 2, 0, 0], rotate = across) cradle(1); part("Plinth", at = [0, 0, -t]) plinth(); } } // Assembly: glue cradles A and B into the plinth, lower the spine into // them, then drop ribs 1 to 9 into the numbered slots on top of the spine. // Dry fit everything before gluing the joints.