Works out where a railing's posts and balusters stand, how long its rails are, and the solids that are written into the model.
How the precision is kept
The path is 64-bit millimeters. The rail length is a sum of 3D segment lengths, and the baluster count is a ceiling with no tolerance. The railing's own path points are stored with 3 decimals of the file's length unit and read back from that text, while its height, base and length are stored with 6 decimals.
Open item. Store the railing's path points with 6 decimals, as its height and base already are, instead of 3.
Calculations under this policy
3 calculations in 1 source file. 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.
Places a post at each path point, spaces the balusters between posts and adds up the rail length.
n = max(0, ⌈(clear − gap) ÷ (gap + b)⌉); s_k = post ÷ 2 + k × (clear − n × b) ÷ (n + 1) + b × (k − ½); length = Σ √(L² + Δz²)
The length is a sum of Math.hypot values added in path order. The baluster count is a ceiling with no tolerance, so a span that is a rounding error over a whole number of spaces gets one more baluster. Each baluster position is computed from the segment start, not from the previous baluster.
res-railing.js
lines 28 to 48
BimCraft 0.8.1
export function railingParts(path3, o = {}) {
const { height, baluster, gap, post, rail } = { ...RAIL_DEFAULTS, ...o };
const rails = [], posts = [], balusters = [];
let length = 0;
const pts = path3.filter((p, i) => i === 0 || Math.hypot(p[0] - path3[i - 1][0], p[1] - path3[i - 1][1]) > 1);
if (pts.length < 2) return { rails, posts, balusters, length, height, baluster, post, rail };
for (let i = 0; i < pts.length; i++) posts.push(pts[i]);
for (let i = 0; i + 1 < pts.length; i++) {
const a = pts[i], b = pts[i + 1], L = Math.hypot(b[0] - a[0], b[1] - a[1]);
length += Math.hypot(L, b[2] - a[2]);
rails.push([[a[0], a[1], a[2] + height], [b[0], b[1], b[2] + height]]);
// balusters between the posts: the clear span (post faces) split so no gap is wider than `gap`
const clear = L - post, n = Math.max(0, Math.ceil((clear - gap) / (gap + baluster)));
for (let k = 1; k <= n; k++) {
const s = post / 2 + (clear - n * baluster) / (n + 1) * k + baluster * (k - 0.5);
const t = s / L;
balusters.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]);
}
}
return { rails, posts, balusters, length, height, baluster, post, rail };
}
Rail solid along a sloped segment
64-bit
Builds each rail as a square bar swept along its 3D segment.
sweep direction = (d̂ · z, d̂_z, d̂ · x) in the frame across the rail
The frame is built from the segment's horizontal direction; a vertical segment would divide by zero, so the horizontal length is replaced by 1. The frame and sweep pass through the model writer, which rounds coordinates to 6 decimals of the file's length unit and directions to 12 decimals.
res-railing.js
lines 61 to 67
BimCraft 0.8.1
const A = P(...a), B = P(...b), d = [B[0] - A[0], B[1] - A[1], B[2] - A[2]], L = Math.hypot(...d);
if (L < 1) continue;
const hz = Math.hypot(d[0], d[1]) || 1, x = [d[0] / hz, d[1] / hz, 0], z = [-x[1], x[0], 0];
const frame = w.axisFrame([A[0] - z[0] * parts.rail / 2, A[1] - z[1] * parts.rail / 2, A[2] - parts.rail / 2], x, z);
const dl = [d[0] / L, d[1] / L, d[2] / L];
const ex = [dl[0] * z[0] + dl[1] * z[1], dl[2], dl[0] * x[0] + dl[1] * x[1]]; // the direction in the frame's axes
items.push(w.extrudeAt(w.profile([[0, 0], [parts.rail, 0], [parts.rail, parts.rail], [0, parts.rail]]), frame, L, ex));
Railing values written to the model
64-bit
Writes the railing's path, height, base, length and counts into the model.
Points = round(x ÷ unit, 3), round(y ÷ unit, 3); Length = round(length ÷ unit, 6)
The path points are written as text rounded to 3 decimals of the file's length unit: 1 mm in a file in meters. readRailings parses that text back, so a railing that is changed later is rebuilt from the rounded points. Height, base and length keep 6 decimals.
Posts at every path point. Between two posts the clear span is split by n = ⌈(clear − gap) ÷ (gap + baluster)⌉ balusters so that no gap is wider than the limit. Rails are square bars swept along each 3D segment.
Number types
64-bit numbers. Default sizes are whole and fractional inches times 25.4, which is exact; the default gap is 4 inches less 0.5 mm.
Tolerances
Path points closer than 1 mm in plan to the one before are dropped. A rail segment shorter than 1 mm makes no solid.
Limits
The baluster gap is measured in plan, not along a sloped rail.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.