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.
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.
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 };
}
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.
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.
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.