Resources Numerical accuracy Calculation policy

Railing posts, balusters and rails

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.

res-railing.js BimCraft 0.8.1 64-bit 3 calculations Reviewed

Tests tools/bimcraft-test/res-test.mjs, tools/bimcraft-test/modify-test.mjs

Posts, balusters and rail length

64-bit

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.

  • res-railing.js
  • lines 88 to 97
  • BimCraft 0.8.1
function writeRailing(w, r) {
  const parts = railingParts(r.points.map(([x, y]) => [x, y, r.base]), { height: r.height });
  const items = railingItems(w, parts);
  const sty = w.surfaceStyle(tr('res.style.rail'), [0.95, 0.95, 0.93], 0);
  for (const it of items) w.styleItem(it, sty);
  w.setShape(r.id, [w.rep('body', 'Body', 'SweptSolid', items)]);
  w.upsertPset(r.id, RAIL_PSET, [['Points', 'IFCTEXT', r.points.map((p) => round(w.u(p[0]), 3) + ',' + round(w.u(p[1]), 3)).join(';')], ['Height', 'IFCPOSITIVELENGTHMEASURE', round(w.u(r.height))], ['Base', 'IFCLENGTHMEASURE', round(w.u(r.base))], ['Kind', 'IFCLABEL', r.kind]]);
  w.upsertPset(r.id, 'CadCraft_RailingQuantities', [['Length', 'IFCLENGTHMEASURE', round(w.u(parts.length))], ['Balusters', 'IFCINTEGER', parts.balusters.length], ['Posts', 'IFCINTEGER', parts.posts.length]]);
  return parts;
}

In detail

Method
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.

Every policy that takes this approach