construction.roof-pitch
Roof pitch in degrees, rafter length and roof slope area from span, rise and length.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 15 tests, run in TypeScript, Python and Rust.
What it does
The pitch of a roof, the length of one rafter and the area of the roof slopes, from the span, the rise and the length along the ridge. All dimensions are in metres.
- **duo** (a pitched roof with a ridge in the middle): the run of each rafter is half the span, and there are two slopes. - **mono** (a lean-to or mono-pitch): the run is the whole span, and there is one slope.
For example
roof_pitch(8, 2, 10, duo)→ pitch degrees 26.57, rafter length 4.472, roof area 89.44 duo pitch, 8 m span, 2 m rise: run 4, tan 0.5roof_pitch(6, 3, 5, duo)→ pitch degrees 45, rafter length 4.243, roof area 42.43 rise equal to the run is 45 degreesroof_pitch(8, 3, 12, duo)→ pitch degrees 36.87, rafter length 5, roof area 120 a 3-4-5 triangle gives an exact 5 m rafter
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn roof_pitch(span: f64, rise: f64, roof_length: f64, roof_type: &str) -> RoofPitch
| span | float | metres, wall plate to wall plate; for a duo pitch the run is half of it |
| rise | float | metres, wall plate to ridge; 0 for a flat roof |
| roof_length | float | metres, along the ridge (gable to gable) |
| roof_type | RoofType | duo: two equal slopes meeting at a ridge; mono: one slope (lean-to) |
| returns | RoofPitch |
The types it declares, generated into your project
// RoofType is a string in Rust, one of: "duo", "mono".
// Parameters take it as &str and results hold it as String.
/// The pitch, one rafter and the whole roof's slope area.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RoofPitch {
/// angle of the slope from horizontal, to 2 decimal places
pub pitch_degrees: f64,
/// metres along the slope from plate to ridge, no overhang, to 3 decimal places (the nearest millimetre)
pub rafter_length: f64,
/// square metres of slope, every slope together, no overhang, to 2 decimal places
pub roof_area: f64,
}
Your code names it in one line, in the file that uses it
fune!(construction.roof-pitch@^1); // then call roof_pitch(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_atan::atan; ← from math.atan ^1.0.0 · built alongside by fune
use super::math_round_float::round_float; ← from math.round-float ^1.0.0 · built alongside by fune
// 180 / pi as a double, written out so no language computes it differently.
const DEGREES_PER_RADIAN: f64 = 57.29577951308232;
const LIMIT: f64 = 10000.0;
fn check_length(name: &str, value: f64, allow_zero: bool) {
let ok = value.is_finite() && value <= LIMIT && if allow_zero { value >= 0.0 } else { value > 0.0 };
if !ok {
let want = if allow_zero { "zero or more" } else { "greater than zero" };
panic!(
"{} must be a finite number of metres, {} and at most 10000, received {}",
name, want, value
);
}
}
/// Pitch, rafter length and slope area of a duo- or mono-pitch roof.
///
/// The area is worked from the unrounded rafter, not the one rounded to a
/// millimetre for display: over a long roof the difference shows in the
/// second decimal place.
///
/// # Panics
/// Panics on a span or roof length that is not above zero, a negative rise,
/// any dimension over 10,000 m, or an unknown roof type.
pub fn roof_pitch(span: f64, rise: f64, roof_length: f64, roof_type: &str) -> RoofPitch {
check_length("span", span, false);
check_length("rise", rise, true);
check_length("roofLength", roof_length, false);
let (run, slopes) = match roof_type {
"duo" => (span / 2.0, 2.0),
"mono" => (span, 1.0),
other => panic!("unknown roof type \"{}\"", other),
};
let radians = atan(rise / run);
// sqrt is one of the operations IEEE 754 requires to be correctly rounded,
// so f64::sqrt is the same double in every language.
let rafter = (run * run + rise * rise).sqrt();
RoofPitch {
pitch_degrees: round_float(radians * DEGREES_PER_RADIAN, 2),
rafter_length: round_float(rafter, 3),
roof_area: round_float(rafter * roof_length * slopes, 2),
}
}
pub fn roof_pitch_to_value(r: &RoofPitch) -> Value {
Value::obj(vec![
("pitchDegrees", Value::Float(r.pitch_degrees)),
("rafterLength", Value::Float(r.rafter_length)),
("roofArea", Value::Float(r.roof_area)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
for (i, name) in ["span", "rise", "roofLength"].iter().enumerate() {
if let Value::Str(s) = &args[i] {
panic!("{} must be a finite number of metres, received \"{}\"", name, s);
}
}
roof_pitch_to_value(&roof_pitch(
args[0].as_f64(),
args[1].as_f64(),
args[2].as_f64(),
args[3].as_str(),
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add construction.roof-pitch
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./construction.roof-pitch-1.0.0-rust.fune, or fetch it from a terminal with fune pull construction.roof-pitch@1.0.0:rust.
The whole function, every language, is one file too: construction.roof-pitch-1.0.0.fune, 13,927 bytes, sha256 a8a35da395a9273cd0e552f4eaf3ea782a8dc8357310c2c616bc80d54187f6f9. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before construction.roof-pitch
after — your function gets the result and the arguments, and returns the final result.
// fune: after construction.roof-pitch
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace math.atan in construction.roof-pitch
// fune: replace math.round-float in construction.roof-pitch
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show construction.roof-pitch --steps.
// fune: step construction.roof-pitch after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| duo pitch, 8 m span, 2 m rise: run 4, tan 0.5 | 8, 2, 10, duo | → | pitch degrees 26.57, rafter length 4.472, roof area 89.44 |
| rise equal to the run is 45 degrees | 6, 3, 5, duo | → | pitch degrees 45, rafter length 4.243, roof area 42.43 |
| a 3-4-5 triangle gives an exact 5 m rafter | 8, 3, 12, duo | → | pitch degrees 36.87, rafter length 5, roof area 120 |
| area from the unrounded rafter: 4.32666 x 40 is 173.07, not 4.327 x 40 = 173.08 | 7.2, 2.4, 20, duo | → | pitch degrees 33.69, rafter length 4.327, roof area 173.07 |
| a rise of 2.887 on a 10 m span is a 30 degree roof | 10, 2.887, 8, duo | → | pitch degrees 30, rafter length 5.774, roof area 92.38 |
| steep duo pitch: tan 6 | 2, 6, 1, duo | → | pitch degrees 80.54, rafter length 6.083, roof area 12.17 |
| mono pitch uses the whole span as the run and has one slope | 4, 1, 3, mono | → | pitch degrees 14.04, rafter length 4.123, roof area 12.37 |
| shallow lean-to | 9.6, 1.2, 15.5, mono | → | pitch degrees 7.13, rafter length 9.675, roof area 149.96 |
| zero rise is a flat roof: pitch 0, rafter equal to the run | 5, 0, 4, mono | → | pitch degrees 0, rafter length 5, roof area 20 |
| the same span as a duo pitch halves the run | 8, 2, 10, mono | → | pitch degrees 14.04, rafter length 8.246, roof area 82.46 |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a zero span is an error | 0, 2, 10, duo | → | error: span must be a finite number of metres |
| a negative rise is an error | 8, -1, 10, duo | → | error: rise must be a finite number of metres |
| a zero roof length is an error | 8, 2, 0, duo | → | error: roofLength must be a finite number of metres |
| a span given as text is an error | 8, 2, 10, duo | → | error: span must be a finite number of metres |
| an unknown roof type is an error | 8, 2, 10, hip | → | error: unknown roof type "hip" |
More from the author
The pitch is `atan(rise / run)` in degrees, the rafter is `sqrt(run² + rise²)`, and the area is rafter × roof length × slopes.
## Rounding
The pitch is rounded to 2 decimal places, the rafter to 3 (the nearest millimetre) and the area to 2, each with `math.round-float`, half away from zero. The area is worked from the **unrounded** rafter: on a 7.2 m span with a 2.4 m rise and a 20 m ridge the rafter is 4.32666 m, so the area is 173.07 m², while 4.327 × 40 would give 173.08.
## Why every language agrees
The angle comes from `math.atan` (fdlibm's arctangent from + − × ÷ only), not `Math.atan`, whose last bit differs between platforms. The square root is the platform's own: IEEE 754 requires square root to be correctly rounded, like + − × ÷, so it is the same double everywhere (`geo.distance` relies on the same guarantee). Degrees are radians × 57.29577951308232, 180/π as a double, written out.
## What it does not do
- Overhangs (eaves and verges), ridge and fascia boards, and the rafter's plumb cuts: the rafter length is plate to ridge along the slope, and the area is the slope over the building's footprint. Add overhang to the run and roof length yourself for tiling or felt quantities, then add waste. - Hips, valleys and unequal pitches. A hipped roof's slope area does equal plan area ÷ cos(pitch) when every slope has the same pitch, but its rafters do not; that is a different calculation. - Pitch from a known angle (the reverse); rise = run × tan(pitch).
A rise of 0 is allowed (a flat roof: pitch 0, rafter equal to the run). Span and roof length must be above zero, and every dimension at most 10,000 m.
Files
| Path | Bytes |
|---|---|
| README.md | 2,075 |
| impl/python.py | 1,916 |
| impl/rust.rs | 2,556 |
| impl/typescript.ts | 1,876 |
| vectors.json | 2,332 |