hex_roof guide

← hex_roof · API reference · source

hex_roof writes roofs, vaults and arches as a HEIGHT per cell — a hex_field::Heights over a HexSet footprint. Every profile is one formula, peak - slope * d, and only the distance d changes: to a point for a cone, to a line for a gable, to a short segment for a hip, to the nearest eave for roof_hip. Going the other way, roof_match reads a height field back as an exact surface, so a renderer draws a real cone rather than a staircase of hexes.

It is not the footprint or its outline — that is hex_field. It is not the ridge line either: a ridge is a hex_way::Track. Reach for hex_roof when cells need a height that follows a roof form.

World units: a cell (q, r) is centred at (lattice_k(q, r) · √3/2, lattice_m(q, r) / 2), so neighbouring centres are √3 apart.

use hex_roof::*;
use hex_field::*;
use hex_way::*;
const SQ3 = 1.7320508075688772;
fn near(a: float, b: float) -> boolean { abs(a - b) < 0.000001 }

A cell and its six neighbours, in a 5 x 5 window around the origin.

fn hexagon() -> HexSet {
  s = hexset_new(2);
  hexset_set(s, 0, 0, true);
  for d in 0..6 { hexset_set(s, nb_q(0, 0, d), nb_r(0, 0, d), true); }
  s
}
fn main() {
  s = hexagon();
  f = heights_new(-2, -2, 5, 5);
  roof_cone(s, f, 0.0, 0.0, 4.0, 1.0, 100.0);      // apex 4 over the origin, slope 1
  fit = roof_match(s, f, 0.001);
  println("apex {height_get(f, 0, 0)}, eave {height_get(f, 1, 0):.4}; recovered as kind {fit.rf_kind}, apex {fit.rf_z:.4}, slope {fit.rf_slope:.4}");
  assert(height_get(f, 0, 0) == 4.0 && near(height_get(f, 1, 0), 4.0 - SQ3), "√3 from the apex");
  assert(fit.rf_kind == ROOF_CONE && near(fit.rf_z, 4.0) && near(fit.rf_slope, 1.0), "the cone back");

Every section below is a function, and main calls each one: a section nothing calls still compiles, but its assertions never run.

  draw_the_surface();
  a_hip_is_a_shorter_ridge();
  a_roof_must_drain();
}

That prints:

  apex 4, eave 2.2679; recovered as kind 2, apex 4.0000, slope 1.0000

(kind 2 is ROOF_CONE.)

Draw the surface, not the cells

The heights are storage: one number per cell. roof_eval(fit, x, y) evaluates the recovered surface anywhere, so the roof is exact at any resolution. When no shape fits within the tolerance, roof_match answers ROOF_UNKNOWN and roof_eval draws nothing — it never passes off the least-bad shape as a recovery.

fn draw_the_surface() {
  s = hexagon();
  f = heights_new(-2, -2, 5, 5);
  roof_cone(s, f, 0.0, 0.0, 4.0, 1.0, 100.0);
  fit = roof_match(s, f, 0.001);
  between = roof_eval(fit, SQ3 / 2.0, 0.0);      // half way to the east neighbour
  assert(near(between, 4.0 - SQ3 / 2.0), "on the cone, between two cell centres");
  println("surface : half way between two cells the roof is {between:.4}");
}

A hip is a shorter ridge

roof_ridge(s, f, track, peak, slope, dmax) measures d to a Track. A ridge the full length of the roof is a gable: the ends stay at full height to the end wall. Shorten the ridge and past its ends the distance runs to the tip instead, so the ends slope in — a hip, from the same call. dmax clamps the distance; 100 here means "never".

fn a_hip_is_a_shorter_ridge() {
  s = hexset_new(4);
  for r in -1..2 { for q in -3..4 { hexset_set(s, q, r, true); } }   // 7 cells by 3 rows
  full = track_new();
  track_straight(full, -3.0 * SQ3, 0.0, 3.0 * SQ3, 0.0);
  short = track_new();
  track_straight(short, 0.0 - SQ3, 0.0, SQ3, 0.0);
  gable = heights_new(-4, -4, 9, 9);
  roof_ridge(s, gable, full, 5.0, 1.0, 100.0);
  hip = heights_new(-4, -4, 9, 9);
  roof_ridge(s, hip, short, 5.0, 1.0, 100.0);
  assert(height_get(gable, 0, 0) == 5.0 && height_get(hip, 0, 0) == 5.0, "both on their ridge");
  assert(height_get(gable, 3, 0) == 5.0, "the gable carries the ridge to the end wall");
  assert(near(height_get(hip, 3, 0), 5.0 - 2.0 * SQ3), "the hip is 2√3 past its ridge's end");
  assert(roof_ponds(s, gable) == 0 && roof_ponds(s, hip) == 0, "both drain");
  println("ridge   : end cell of the gable {height_get(gable, 3, 0)}, of the hip {height_get(hip, 3, 0):.4}");
}

A roof must drain

roof_ponds counts INTERIOR cells — all six neighbours in the footprint — with no lower neighbour: water collects there and never leaves. A roof that drains has none. Boundary cells are not counted, because they drain off the eave.

roof_hip(s, f, eave, per_ring) is the roof that drains on any footprint: the boundary is ring 0 at eave height, and each ring inward rises by per_ring. Its eave is level by construction, which eave_spread (the spread of the boundary heights) confirms.

fn a_roof_must_drain() {
  s = hexagon();
  f = heights_new(-2, -2, 5, 5);
  top = roof_hip(s, f, 3.0, 1.0);
  assert(top == 1 && height_get(f, 0, 0) == 4.0, "one ring inside the eave");
  assert(eave_spread(s, f) == 0.0 && roof_ponds(s, f) == 0, "a level eave, and it drains");
  height_set(f, 0, 0, 2.0);            // dent the middle below the eave
  assert(roof_ponds(s, f) == 1, "the centre now holds water");
  println("drain   : hip rings {top}, ponds 0; dented, ponds {roof_ponds(s, f)}");
}

Where to go next