Models / Hadal skeleton sculpture

Hadal skeleton sculpture

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.

Download size: 497.7 × 304.7 mm. 13 parts on 1 sheet.

Source 174 lines

// 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.