Models / Wave Cathedral

Wave Cathedral

Wave Cathedral: a vault made of tides. Seventeen flat arches, each a little different, slot onto two notched rails on a base. Two crests run along the roof and the crown drifts from side to side, so the flat ribs read as a rolling wave.

20 parts on 5 sheets, shown together. Each sheet has its own cut file.

Source 147 lines

// Wave Cathedral: a vault made of tides. Seventeen flat arches, each a little
// different, slot onto two notched rails on a base. Two crests run along the
// roof and the crown drifts from side to side, so the flat ribs read as a
// rolling wave.
 
/* [Pavilion] */
// Length of the base
length = 320; // [280:10:340]
// Height of the roof waves and how far the crown drifts
wave = 1; // [0.8:0.1:1.2]
// All parts, one group at a time, or three pieces to test the slot fit
output = "all"; // [all:All parts, ribs_a:Ribs 1 to 4, ribs_b:Ribs 5 to 8, ribs_c:Ribs 9 to 12, ribs_d:Ribs 13 to 16, rib_e:Rib 17, base:Base, rails:Rails, fit:Fit test]
// Scored contour lines, rib numbers and base markings
score_detail = true;
 
/* [Material] */
// Sheet thickness
$thickness = 3; // [2.8:0.1:3.2]
// Added to every slot, for a glued slip fit
clearance = 0.15; // [0.10:0.05:0.30]
// Beam width
$kerf = 0.15; // [0:0.01:0.25]
 
/* [Hidden] */
$material = "plywood";
$sheet = [600, 400];
$gap = 6;
$fn = 120;
s = length / 320;
t = $thickness;
sw = t + clearance;
rail_x = 100 * s;
rail_h = 24 * s;
seat = 12 * s;
 
// Where rib i stands along the base
function station(i) = s * (16 + 18 * i);
// Roof height along the nave (v from 0 at the front to 1 at the back): two crests
function crest(v) = s * (100 + wave * (58 * exp(-pow((v - 0.22) / 0.18, 2)) + 36 * exp(-pow((v - 0.82) / 0.20, 2))));
// How far the crown leans sideways
function drift(v) = s * wave * 29 * sin(360 * v - 32);
// A point on rib i at angle a: every rib has the same feet, its own crown
function arch(i, a, inner = false) = let (v = i / 16, r = (inner ? 88 : 112) * s, h = crest(v) - (inner ? 18 * s : 0), q = max(0, sin(a)))
  [r * cos(a) + drift(v) * q * q, h * pow(q, 0.86)];
function trace(i, a) = let (v = i / 16, q = max(0, sin(a)))
  [100 * s * cos(a) + drift(v) * q * q, (crest(v) - 9 * s) * pow(q, 0.86)];
 
module rib(i) {
  difference() {
    polygon(concat([for (j = [0:240]) arch(i, j * 180 / 240)],
      [for (j = [240:-1:0]) arch(i, j * 180 / 240, true)]));
    // Slots from below, over the slots from above in the rails
    for (side = [-1, 1]) translate([side * rail_x - sw / 2, -1]) rect([sw, seat + 1]);
  }
  if (score_detail) score() {
    polyline([for (j = [10:230]) trace(i, j * 180 / 240)]);
    translate([-100 * s, 26 * s]) text(str(i + 1), size = 3 * s, halign = "center", valign = "center");
    polyline([[95 * s, 25 * s], [103 * s, 25 * s], [100 * s, 28 * s]]);
  }
}
 
module rail(label) {
  difference() {
    union() {
      translate([8 * s, 0]) rect([length - 16 * s, rail_h]);
      for (y = [40 * s, 160 * s, 280 * s]) translate([y - 6 * s, -t]) rect([12 * s, t + 0.3]);
    }
    for (i = [0:16]) translate([station(i) - sw / 2, seat]) rect([sw, rail_h - seat + 1]);
  }
  if (score_detail) score() {
    for (i = [0:16]) translate([station(i), 6 * s]) text(str(i + 1), size = 2.5 * s, halign = "center", valign = "center");
    translate([151 * s, 18 * s]) text(label, size = 3 * s, halign = "center", valign = "center");
    line([20 * s, 3 * s], [length - 20 * s, 3 * s]);
  }
}
 
module base() {
  difference() {
    translate([-126 * s, 0]) rect([252 * s, length], r = 8 * s);
    for (side = [-1, 1], y = [40 * s, 160 * s, 280 * s])
      translate([side * rail_x, y]) rect([sw, 12 * s + clearance], center = true);
  }
  if (score_detail) score() {
    translate([0, 7.5 * s]) text("W A V E  C A T H E D R A L", size = 3.4 * s, halign = "center", valign = "center");
    translate([0, length - 7 * s]) text("A VAULT MADE OF TIDES / XVII", size = 2.7 * s, halign = "center", valign = "center");
    for (side = [-1, 1]) polyline([for (j = [0:100]) let (v = j / 100) [side * (54 + 5 * sin(360 * v)) * s, (20 + 280 * v) * s]]);
    for (i = [0:16], side = [-1, 1]) {
      translate([side * 119 * s, station(i)]) text(str(i + 1), size = 2.4 * s, halign = "center", valign = "center");
      line([side * 114 * s, station(i)], [side * 116 * s, station(i)]);
    }
    translate([-100 * s, 7 * s]) text("L", size = 3 * s, halign = "center", valign = "center");
    translate([100 * s, 7 * s]) text("R", size = 3 * s, halign = "center", valign = "center");
    polyline([[-12 * s, 22 * s], [12 * s, 22 * s], [8 * s, 25 * s]]);
    translate([0, 28 * s]) text("+X / FRONT", size = 2.5 * s, halign = "center", valign = "center");
  }
}
 
module fit_test() {
  part("Fit test, base") {
    difference() {
      rect([48, 30], r = 2);
      translate([24, 17]) rect([sw, 12 * s + clearance], center = true);
    }
    if (score_detail) score() translate([24, 6]) text("BASE", size = 3, halign = "center");
  }
  part("Fit test, rail") {
    difference() {
      union() {
        rect([36, rail_h]);
        translate([18 - 6 * s, -t]) rect([12 * s, t + 0.3]);
      }
      translate([18 - sw / 2, seat]) rect([sw, rail_h - seat + 1]);
    }
    if (score_detail) score() translate([7, 6]) text("R", size = 3);
  }
  part("Fit test, rib") {
    difference() {
      rect([30, 30 * s]);
      translate([15 - sw / 2, -1]) rect([sw, seat + 1]);
    }
    if (score_detail) score() translate([6, 21 * s]) text("V", size = 3);
  }
}
 
// Steps are numbered in the order they first run, so name them here in build order
step("Glue the rails into the base, numbers running from the front") {}
step("Lower the ribs into their numbered notches, scored faces to the front") {}
// Assembled: the rails stand in the base, the ribs across them at their
// stations
module placed_rib(i) step("Lower the ribs into their numbered notches, scored faces to the front") part(str("Rib ", i + 1), at = [0, station(i) + t / 2, 0], rotate = [90, 0, 0]) rib(i);
if (output == "all" || output == "ribs_a") for (i = [0:3]) placed_rib(i);
if (output == "all" || output == "ribs_b") for (i = [4:7]) placed_rib(i);
if (output == "all" || output == "ribs_c") for (i = [8:11]) placed_rib(i);
if (output == "all" || output == "ribs_d") for (i = [12:15]) placed_rib(i);
if (output == "all" || output == "rib_e") placed_rib(16);
if (output == "all" || output == "rails") {
  step("Glue the rails into the base, numbers running from the front") {
    part("Rail L", at = [-rail_x - t / 2, 0, 0], rotate = [90, 0, 90]) rail("L");
    part("Rail R", at = [rail_x - t / 2, 0, 0], rotate = [90, 0, 90]) rail("R");
  }
}
if (output == "all" || output == "base") step("Glue the rails into the base, numbers running from the front") part("Base", at = [0, 0, -t]) base();
if (output == "fit") fit_test();
 
// Assembly: glue rail L and rail R into the base, numbers running from the
// front. Lower ribs 1 to 17 into the matching notches, scored faces to the
// front, then glue all the joints after a dry fit.