Resources Numerical accuracy Calculation policy

Truss layout over a roof

Sets out the trusses along a roof: where each one stands, its span, and which trusses share a design, including hip-end girders and jacks and valley sets.

How the precision is kept

Stations and spans are computed in 64-bit plan millimeters by cutting the roof outline with each truss line. The values stored for each truss are then rounded: station and end points to 0.1 mm and span to a whole millimeter. Trusses are grouped into designs by rounded span and pitch, and a group is designed with the unrounded numbers of its first truss.

Open item. Store each truss's station, end points and span unrounded and round only for the design key and for display.

Calculations under this policy

9 calculations in 2 source files. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

addons/trusscraft/lib/layout.js CadCraft 0.8.2 64-bit 3 calculations Reviewed

Tests tools/addons-test/browser-trusscraft-cadcraft.mjs

Outline area

64-bit

The signed area of the roof outline.

A = ½ Σ (xᵢ yᵢ₊₁ − xᵢ₊₁ yᵢ)

The shoelace sum over absolute coordinates. For an outline far from the origin the products are large and partly cancel. The layout uses the result for its sign and for threshold tests of 1e6 and 1e4 mm².

  • addons/trusscraft/lib/layout.js
  • line 13
  • CadCraft 0.8.2
export function polyArea(p) { let s = 0; for (let i = 0; i < p.length; i++) { const a = p[i], b = p[(i + 1) % p.length]; s += a[0] * b[1] - a[1] * b[0]; } return s / 2; }

Where a truss line crosses the outline

64-bit

The points where a station line enters and leaves the roof outline.

v = a_v + (u − a_u) ÷ (b_u − a_u) × (b_v − a_v)

An edge is crossed when one end is at or below the station and the other above it, so a shared corner is counted once. The crossing is a linear interpolation along the edge.

  • addons/trusscraft/lib/layout.js
  • lines 24 to 32
  • CadCraft 0.8.2
/** The v values where the line u = u0 crosses the polygon, sorted (u, v in the frame of ridge direction ru and span direction rv). */
function crossings(P, u0) {
  const xs = [];
  for (let i = 0; i < P.length; i++) {
    const a = P[i], b = P[(i + 1) % P.length];
    if ((a[0] <= u0 && b[0] > u0) || (b[0] <= u0 && a[0] > u0)) xs.push(a[1] + ((u0 - a[0]) / (b[0] - a[0])) * (b[1] - a[1]));
  }
  return xs.sort((p, q) => p - q);
}

Trusses along an outline

64-bit

The station, end points and span of each truss over a plain outline, and the actual spacing.

bays = ceil(length ÷ spacing); step = length ÷ bays; span = v₁ − v₀

The two end stations are moved 0.001 mm inward before the outline is cut. Each truss's station and end points are rounded to 0.1 mm and its span to a whole millimeter before they are stored. Trusses whose rounded spans differ by 1 mm or less form one group.

  • addons/trusscraft/lib/layout.js
  • lines 34 to 73
  • CadCraft 0.8.2
export function layoutTrusses(outline, { spacing = 600, ridge = 'auto', inset = 19, gables = null, minSpan = 1500 } = {}) {
  if (!Array.isArray(outline) || outline.length < 3 || Math.abs(polyArea(outline)) < 1e6) return { ok: false, error: 'outline' };
  if (!(spacing >= 200 && spacing <= 3000)) return { ok: false, error: 'spacing' };
  const poly = polyArea(outline) < 0 ? [...outline].reverse() : outline.map((q) => [q[0], q[1]]);
  const le = longestEdge(poly);
  let ru;
  if (ridge === 'x') ru = [1, 0]; else if (ridge === 'y') ru = [0, 1];
  else if (Number.isFinite(Number(ridge)) && ridge !== 'auto' && ridge !== '') { const a = Number(ridge) * Math.PI / 180; ru = [Math.cos(a), Math.sin(a)]; } else ru = le.d;
  const rv = [-ru[1], ru[0]];
  const P = poly.map((q) => [q[0] * ru[0] + q[1] * ru[1], q[0] * rv[0] + q[1] * rv[1]]);
  const u0 = Math.min(...P.map((q) => q[0])), u1 = Math.max(...P.map((q) => q[0]));
  const L = u1 - u0 - 2 * inset;
  if (L <= 0) return { ok: false, error: 'outline' };
  const n = Math.max(1, Math.ceil(L / spacing)), step = L / n, issues = [];
  const trusses = [];
  let k = 0;
  for (let i = 0; i <= n; i++) {
    const u = u0 + inset + i * step, xs = crossings(P, u + (i === 0 ? 1e-3 : i === n ? -1e-3 : 0));
    for (let j = 0; j + 1 < xs.length; j += 2) {
      const v0 = xs[j], v1 = xs[j + 1], span = v1 - v0;
      if (span < minSpan) continue;
      k++;
      const w = (v) => [u * ru[0] + v * rv[0], u * ru[1] + v * rv[1]];
      trusses.push({ id: 'T' + k, u: Math.round(u * 10) / 10, v0, v1, span: Math.round(span), a: w(v0).map((x) => Math.round(x * 10) / 10), b: w(v1).map((x) => Math.round(x * 10) / 10), end: i === 0 || i === n });
    }
  }
  if (!trusses.length) return { ok: false, error: 'outline' };
  const groups = [];
  for (const t of trusses) { const g = groups.find((x) => Math.abs(x.span - t.span) <= 1); if (g) g.ids.push(t.id); else groups.push({ span: t.span, ids: [t.id] }); }
  // a rectangle gives one span (to a millimetre); anything else is flagged
  const rect = polyArea(poly) > 0 && Math.abs(Math.abs(polyArea(P)) - (u1 - u0) * (Math.max(...P.map((q) => q[1])) - Math.min(...P.map((q) => q[1])))) < 1e4;
  if (!rect) issues.push({ code: 'notrect', vars: { spans: groups.length } });
  else if (Array.isArray(gables)) {
    // the roof's gable edges are numbered by the outline's edges; the two ends across the ridge must both be gables for a pure gable roof
    const ends = poly.map((a, i) => { const b = poly[(i + 1) % poly.length]; return Math.abs((b[0] - a[0]) * ru[0] + (b[1] - a[1]) * ru[1]) < 1e-3 * Math.hypot(b[0] - a[0], b[1] - a[1]) ? i : -1; }).filter((i) => i >= 0);
    if (!ends.every((i) => gables.includes(i))) issues.push({ code: 'hip', vars: {} });
  }
  if (groups.some((g) => g.span > 12000)) issues.push({ code: 'longspan', vars: { span: Math.max(...groups.map((g) => g.span)) } });
  return { ok: true, ridgeDir: ru, spanDir: rv, trusses, spacing: Math.round(step * 10) / 10, groups, issues, count: trusses.length };
}
addons/trusscraft/lib/roofset.js CadCraft 0.8.2 64-bit 6 calculations Reviewed

Tests tools/trusscraft/test-mouth.mjs

Ridge direction

64-bit

The direction the ridge runs, which the trusses span across.

The direction is the longest ridge line of the roof plan, or else the longest outline edge, divided by its Math.hypot length. A typed angle is converted with π ÷ 180 and cos and sin.

  • addons/trusscraft/lib/roofset.js
  • lines 31 to 42
  • CadCraft 0.8.2
/** Ridge direction: the longest ridge line of the roof plan, else the longest outline edge. */
export function ridgeDirection(roof, ridge = 'auto') {
  if (ridge === 'x') return [1, 0];
  if (ridge === 'y') return [0, 1];
  if (ridge !== 'auto' && ridge !== '' && Number.isFinite(Number(ridge))) { const a = Number(ridge) * D2R; return [Math.cos(a), Math.sin(a)]; }
  const rl = (roof.plan && roof.plan.lines ? roof.plan.lines : []).filter((l) => l.kind === 'ridge').sort((a, b) => b.len - a.len)[0];
  if (rl) return norm([rl.b[0] - rl.a[0], rl.b[1] - rl.a[1]]);
  let best = null;
  const o = roof.outline;
  for (let i = 0; i < o.length; i++) { const a = o[i], b = o[(i + 1) % o.length], l = Math.hypot(b[0] - a[0], b[1] - a[1]); if (!best || l > best.l) best = { l, d: norm([b[0] - a[0], b[1] - a[1]]) }; }
  return best.d;
}

Valley pairs of a roof plan

64-bit

Finds pairs of valley lines that meet at a wing's ridge, with the wing's axis and reach.

Two valley lines form a pair when their high ends are within 3 mm, and a ridge line belongs to them when one of its ends is within 3 mm in plan. The alignment value is the cosine between the wing axis and the line from the top to the middle of the two low ends.

  • addons/trusscraft/lib/roofset.js
  • lines 44 to 63
  • CadCraft 0.8.2
export function detectValleys(plan) {
  const lines = plan && plan.lines ? plan.lines : [], vs = lines.filter((l) => l.kind === 'valley'), out = [];
  const high = (l) => (l.a[2] >= l.b[2] ? [l.a, l.b] : [l.b, l.a]);
  const used = new Set();
  vs.forEach((v, i) => {
    if (used.has(i)) return;
    const [t1, q1] = high(v);
    const j = vs.findIndex((w, k) => k !== i && !used.has(k) && Math.hypot(...high(w)[0].map((c, n) => c - t1[n])) < 3);
    if (j < 0) return;
    used.add(i); used.add(j);
    const [, q2] = high(vs[j]);
    const rl = lines.find((l) => l.kind === 'ridge' && (Math.hypot(l.a[0] - t1[0], l.a[1] - t1[1]) < 3 || Math.hypot(l.b[0] - t1[0], l.b[1] - t1[1]) < 3));
    if (!rl) return;
    const far = Math.hypot(rl.a[0] - t1[0], rl.a[1] - t1[1]) < 3 ? rl.b : rl.a, e = norm([far[0] - t1[0], far[1] - t1[1]]);
    const mid = [(q1[0] + q2[0]) / 2, (q1[1] + q2[1]) / 2], dq = [mid[0] - t1[0], mid[1] - t1[1]], dist = Math.hypot(...dq);
    const aligned = dist > 1 ? Math.abs((dq[0] * e[0] + dq[1] * e[1]) / dist) : 0;
    out.push({ top: t1, ends: [q1, q2], e, ridgeZ: t1[2], aligned, reach: dist, wingAtEave: Math.hypot(q1[0] - q2[0], q1[1] - q2[1]) });
  });
  return out;
}

Valley truss set

64-bit

The span, rise, base height and position of each valley truss of a wing roof over a main roof.

ridge height = (W ÷ 2) tan(wing pitch); rise = d × tan(main pitch); span = 2 d tan(main pitch) ÷ tan(wing pitch)

Stations are at the full distance minus whole multiples of the spacing. The stored values are rounded: distance, rise, base height and end points to 0.1 mm and span to a whole millimeter. The set stops where the rise falls below the minimum height or the span below 600 mm.

  • addons/trusscraft/lib/roofset.js
  • lines 65 to 102
  • CadCraft 0.8.2
/**
 * The valley trusses of a wing roof over a main roof. Typed: { wingWidth, wingPitch, mainPitch, spacing, minHeight } (the wing's ridge height
 * (W2/2) tan(wing pitch) above the plate; it meets the main roof plane x0 = R2 / tan(main pitch) up the slope). From a plan: { valley: detectValleys()[i] }.
 * Each truss is a triangle on a level base standing on the main roof, spanning the wing between the valley lines at that height, its apex on the wing ridge.
 */
export function valleySet(p = {}) {
  const spacing = p.spacing > 0 ? p.spacing : 600, minH = p.minHeight > 0 ? p.minHeight : 300, out = [], issues = [];
  let R2, s1, dmax, span = (z) => 0, place = null;
  if (p.valley) {
    const v = p.valley;
    if (v.aligned < 0.95) return { ok: false, error: 'valleyskew', trusses: [], issues };
    R2 = v.ridgeZ; const dQ = v.reach, zQ = (v.ends[0][2] + v.ends[1][2]) / 2;
    s1 = (R2 - zQ) / dQ;      // the main roof's fall per mm along the wing axis, from the valley lines
    dmax = dQ;
    span = (d) => { const t = d / dQ, q1 = v.ends[0], q2 = v.ends[1]; return Math.hypot((q1[0] - v.top[0]) * t - (q2[0] - v.top[0]) * t, (q1[1] - v.top[1]) * t - (q2[1] - v.top[1]) * t); };
    place = (d) => { const t = d / dQ, q1 = v.ends[0], q2 = v.ends[1]; return { a: [v.top[0] + (q1[0] - v.top[0]) * t, v.top[1] + (q1[1] - v.top[1]) * t], b: [v.top[0] + (q2[0] - v.top[0]) * t, v.top[1] + (q2[1] - v.top[1]) * t] }; };
  } else {
    const W2 = Number(p.wingWidth), pw = Number(p.wingPitch), pm = Number(p.mainPitch);
    if (!(W2 >= 1500) || !(pw >= 5 && pw <= 60) || !(pm >= 5 && pm <= 60)) return { ok: false, error: 'valleyparams', trusses: [], issues };
    R2 = (W2 / 2) * Math.tan(pw * D2R); s1 = Math.tan(pm * D2R);
    dmax = R2 / s1;
    span = (d) => (2 * d * s1) / Math.tan(pw * D2R);
    place = (d) => ({ a: [-span(d) / 2, d], b: [span(d) / 2, d] });
  }
  const dFull = p.dFull > 0 ? p.dFull : dmax;
  // stations from the wall end up toward the ridge junction (distance d from the top point along the wing axis)
  for (let k = 1; ; k++) {
    const d = dFull - k * spacing;
    if (d <= 0) break;
    const rise = d * s1, S = span(d);
    if (rise < minH) { issues.push({ code: 'valleylow', vars: { from: Math.round(d), rise: Math.round(rise) } }); break; }
    if (S < 600) break;
    const pl = place(d);
    out.push({ d: r1(d), span: Math.round(S), rise: r1(rise), zBase: r1(R2 - rise), a: pl.a.map(r1), b: pl.b.map(r1) });
  }
  if (!out.length) return { ok: false, error: 'valleyempty', trusses: [], issues };
  return { ok: true, trusses: out, ridgeZ: R2, fall: s1, issues };
}

Stations along the ridge

64-bit

The truss stations of a roof and the stretches of each station that lie inside the outline.

bays = ceil(length ÷ spacing); u = u₀ + inset + i × step

The spacing actually used is the length divided by the whole number of bays. The first and last station lines are moved 0.001 mm inward before the outline is cut, and stretches shorter than the minimum span are dropped.

  • addons/trusscraft/lib/roofset.js
  • lines 125 to 130
  • CadCraft 0.8.2
const n = Math.max(1, Math.ceil(L / spacing)), step = L / n;
const stations = [];
for (let i = 0; i <= n; i++) {
  const u = u0 + inset + i * step, xs = crossings(P, u + (i === 0 ? 1e-3 : i === n ? -1e-3 : 0)), iv = [];
  for (let j = 0; j + 1 < xs.length; j += 2) if (xs[j + 1] - xs[j] >= minSpan) iv.push([xs[j], xs[j + 1]]);
  stations.push({ i, u, iv, end: i === 0 || i === n });

A truss from a roof section

64-bit

Decides the form of the truss at a station, its design key, and the values stored for it.

common truss pitch = atan(2 × apex ÷ span)

Trusses share a design when their span rounds to the same millimeter and their pitch to the same 0.01°; the design keeps the unrounded span and pitch of the first truss with that key. For other forms the profile is rounded to 0.1 mm per point. Each truss stores its span as a whole millimeter and its ends to 0.1 mm.

  • addons/trusscraft/lib/roofset.js
  • lines 160 to 181
  • CadCraft 0.8.2
const trussFrom = (s, j, kind, extra = {}) => {
  const [v0, v1] = s.iv[j], pts = s.sec[j], S = v1 - v0;
  if (!pts || pts.length < 2) { failures.push({ station: s.i, u: r1(s.u), error: 'profile', label }); return null; }
  const c = classify(pts);
  let form = extra.form;
  if (!form) form = c.triangle && c.symmetric && c.endsLow ? 'common' : c.flats.length && c.endsLow && !c.notches && !c.vertexNotches ? 'stepdown' : 'profile';
  if (c.notches || c.vertexNotches) issues.push({ code: 'valleycross', vars: { u: Math.round(s.u) } });
  const a = w(s.u, v0).map(r1), b = w(s.u, v1).map(r1);
  if (form === 'common') {
    const pitch = Math.atan((2 * c.apex) / S) / D2R;
    if (pitch >= 5 && pitch <= 60) {
      const type = o.type || 'fink', key = `c|${type}|${Math.round(S)}|${pitch.toFixed(2)}`;
      addDesign(key, { kind: 'common', spec: { ...base, type, span: S, pitch, overhang: oh }, trib: step });
      return make('common', { u: r1(s.u), span: Math.round(S), a, b, end: s.end, key, zBase: 0, label, v0, v1 });
    }
    form = 'profile';
  }
  const prof = pts.map((q) => [r1(q[0]), r1(q[1])]), sig = prof.map((q) => q.map(Math.round).join(',')).join(';');
  const key = `p|${form}|${Math.round(S)}|${sig}|${Math.round(extra.trib || step)}${extra.girder ? '|g' : ''}`;
  addDesign(key, { kind: form, spec: { form, span: S, profile: prof, overhangL: pts[0][1] < 1 ? oh : 0, overhangR: pts[pts.length - 1][1] < 1 ? oh : 0, heel: base.heel, panels: base.panels }, trib: extra.trib || step, extraX: extra.extraX || [], pointFrom: extra.pointFrom || [] });
  return make(form, { u: r1(s.u), span: Math.round(S), a, b, end: s.end, key, zBase: 0, label, v0, v1 });
};

Jack trusses in a hip end

64-bit

The jack and corner trusses between the end wall and a girder truss, and where each bears on the girder.

count = ceil((width − 2 × inset) ÷ spacing); v = v₀ + inset + k × step

Each jack's profile is cut from the roof along its own line and rounded to 0.1 mm per point, and its span is the profile's last x, stored as a whole millimeter. A jack lower than the minimum height, or one that starts more than 1 mm above the plate, is left out and counted.

  • addons/trusscraft/lib/roofset.js
  • lines 186 to 198
  • CadCraft 0.8.2
const [v0, v1] = iv, W = v1 - v0, nJ = Math.max(1, Math.ceil((W - 2 * inset) / spacing)), sj = (W - 2 * inset) / nJ;
const pf = [], extraX = [], jackIds = [];
let small = 0;
for (let k = 0; k <= nJ; k++) {
  const v = v0 + inset + k * sj;
  const pts = sectionProfile(plan, w(gd.uEnd, v), w(g.u, v));
  if (pts.length < 2) { failures.push({ station: g.i, u: r1(g.u), error: 'profile', label }); continue; }
  const top = Math.max(...pts.map((q) => q[1]));
  if (top < minHeight || pts[0][1] > 1) { small++; continue; }
  const L1 = pts[pts.length - 1][0], c = classify(pts), form = c.flats.length ? 'corner' : 'jack';
  const prof = pts.map((q) => [r1(q[0]), r1(q[1])]), sig = prof.map((q) => q.map(Math.round).join(',')).join(';');
  const key = `j|${form}|${Math.round(L1)}|${sig}|${Math.round(sj)}`;
  addDesign(key, { kind: form, spec: { form, span: L1, profile: prof, overhangL: oh, overhangR: 0, heel: 0, panels: 0 }, trib: sj });

In detail

Method
Outline rotated into ridge and span axes; stations at equal steps no larger than the spacing; each station line cut by the outline polygon; roof section from the roof plan for each stretch; grouping by a key of rounded values.
Number types
64-bit numbers; plan millimeters.
Tolerances
End stations moved 0.001 mm inward. Outline area must exceed 1e6 mm². Rectangle test: areas within 1e4 mm². Valley pairing: 3 mm. Wing axis alignment: cosine at least 0.95. Design key: span to 1 mm, pitch to 0.01°, profile points to 1 mm.
Limits
Stored station and end points carry 0.1 mm and spans 1 mm. A design shared by a group uses its first truss's span and pitch. Valleys, hips and the trusses they need are made only in the cases the file header lists; other stations are reported as issues.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach