Resources Numerical accuracy Calculation policy

Grid lines

Lays out grid lines by spacing and angle, finds where they cross for snapping, and decides which lines an elevation sees end on.

How the precision is kept

Line positions are running 64-bit sums of the spacings, turned by one cosine and sine. Crossings come from the two-line parametric solve with a relative test for parallel lines. Points are moved between the story's plan, the building's axes and the grid's own axes with 64-bit 4 × 4 matrices; the shared model writer rounds the stored points to 6 decimals of the file's unit.

Calculations under this policy

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

model-grids.js BimCraft 0.8.1 64-bit 6 calculations Reviewed

Tests tools/bimcraft-test/draw06-test.mjs

Grid lines by spacing

64-bit

The ends of numbered and lettered grid lines from a count, a spacing or list of spacings, an origin and an angle.

xᵢ = xᵢ₋₁ + spacing; P = origin + R(angle) × (x, y)

Each line's position is the one before plus a spacing, a running sum, so rounding can build up over many lines of a spacing that is not exact in binary. The angle in degrees is multiplied by π ÷ 180 for one cosine and sine.

  • model-grids.js
  • lines 61 to 74
  • BimCraft 0.8.1
export function gridLayout({ origin = [0, 0], angle = 0, u = {}, v = {}, extend = 1500 } = {}) {
  const at = (sp, n) => { const out = [0]; for (let i = 1; i < n; i++) out.push(out[i - 1] + (Array.isArray(sp) ? Number(sp[i - 1] ?? sp[sp.length - 1]) : Number(sp))); return out; };
  const nu = Math.max(0, Math.floor(u.count || 0)), nv = Math.max(0, Math.floor(v.count || 0));
  const xs = at(u.spacing ?? 6000, nu), ys = at(v.spacing ?? 6000, nv);
  const xMax = xs.length ? xs[xs.length - 1] : 0, yMax = ys.length ? ys[ys.length - 1] : 0;
  const c = Math.cos(angle * Math.PI / 180), s = Math.sin(angle * Math.PI / 180);
  const P = (x, y) => [origin[0] + x * c - y * s, origin[1] + x * s + y * c];
  const out = [];
  let tg = u.first || '1';
  for (const x of xs) { out.push({ tag: tg, a: P(x, -extend), b: P(x, yMax + extend), fam: 'u' }); tg = nextTag(tg); }
  tg = v.first || 'A';
  for (const y of ys) { out.push({ tag: tg, a: P(-extend, y), b: P(xMax + extend, y), fam: 'v' }); tg = nextTag(tg); }
  return out;
}

Where grid lines cross

64-bit

The crossing points of every pair of grid lines, for snapping.

den = r × s; t = (q − p) × s ÷ den; point = p + t r

The parallel test is scaled by both lines' lengths, so it is a limit on the sine of the angle between them (1e-6). The parameters are accepted within 1e-6 beyond the segment's ends; the point is computed from the first line.

  • model-grids.js
  • lines 123 to 134
  • BimCraft 0.8.1
export function gridSnap(axes) {
  const lines = axes.map((x) => ({ a: x.a, b: x.b })), points = [];
  for (let i = 0; i < axes.length; i++) for (let j = i + 1; j < axes.length; j++) {
    const p = axes[i].a, r = [axes[i].b[0] - p[0], axes[i].b[1] - p[1]], q = axes[j].a, s = [axes[j].b[0] - q[0], axes[j].b[1] - q[1]];
    const den = r[0] * s[1] - r[1] * s[0];
    if (Math.abs(den) < 1e-6 * Math.hypot(...r) * Math.hypot(...s)) continue;
    const t = ((q[0] - p[0]) * s[1] - (q[1] - p[1]) * s[0]) / den, u = ((q[0] - p[0]) * r[1] - (q[1] - p[1]) * r[0]) / den;
    if (t < -1e-6 || t > 1 + 1e-6 || u < -1e-6 || u > 1 + 1e-6) continue;
    points.push([p[0] + r[0] * t, p[1] + r[1] * t]);
  }
  return { lines, points };
}

Grid lines an elevation shows

64-bit

The lines that run within 30° of a view's direction, and where they sit across the view.

kept when |e · d| ÷ |e| ≥ 0.866; x = a · right

0.866 stands for cos 30°, a rounded figure that sets the limit at about 30.003°. The view direction is divided by its length without a check for zero.

  • model-grids.js
  • lines 136 to 144
  • BimCraft 0.8.1
export function gridsInView(axes, dir) {
  const l = Math.hypot(dir[0], dir[1]), d = [dir[0] / l, dir[1] / l], right = [d[1], -d[0]], out = [];
  for (const x of axes) {
    const e = [x.b[0] - x.a[0], x.b[1] - x.a[1]], m = Math.hypot(...e);
    if (!m || Math.abs(e[0] * d[0] + e[1] * d[1]) / m < 0.866) continue;
    out.push({ tag: x.tag, x: x.a[0] * right[0] + x.a[1] * right[1] });
  }
  return out.sort((p, q) => p.x - q.x);
}

Is the grid rectangular

64-bit

A yes or no that decides the grid's type in the file.

parallel when |d₁ × d₂| < 1e-6; square when |u · v| < 1e-6

Unit directions are compared with an absolute 1e-6 on their cross product (parallel) and dot product (square).

  • model-grids.js
  • lines 197 to 203
  • BimCraft 0.8.1
function isRectangular(axes) {
  const dir = (x) => { const d = [x.b[0] - x.a[0], x.b[1] - x.a[1]], l = Math.hypot(...d); return [d[0] / l, d[1] / l]; };
  const U = axes.filter((x) => x.fam !== 'v').map(dir), V = axes.filter((x) => x.fam === 'v').map(dir);
  if (!U.length || !V.length) return false;
  const par = (p, q) => Math.abs(p[0] * q[1] - p[1] * q[0]) < 1e-6;
  return U.every((d) => par(d, U[0])) && V.every((d) => par(d, V[0])) && Math.abs(U[0][0] * V[0][0] + U[0][1] * V[0][1]) < 1e-6;
}

Grid line ends read from the file

64-bit

A grid line's two ends in building millimeters, from its polyline and the grid's placement.

world = M × (x × unit, y × unit, 0)

Coordinates are multiplied by the millimeters per file unit and taken through the grid's world matrix in 64-bit. A line stored against its sense is turned round.

  • model-grids.js
  • lines 107 to 108
  • BimCraft 0.8.1
const ends = val(ax.SameSense) === false ? [pts[pts.length - 1], pts[0]] : [pts[0], pts[pts.length - 1]];
const [a, b] = ends.map((q) => { const r = apply4(M, q[0], q[1], 0); return [r[0], r[1]]; });

Grid lines in a story's plan

64-bit

Grid line ends moved from building coordinates into one story's plan axes.

plan = (story world)⁻¹ × point

One general 4 × 4 inverse of the story's world matrix, then one matrix product per end.

  • model-grids.js
  • lines 117 to 121
  • BimCraft 0.8.1
export function toPlan(w, storey, grids) {
  const Mi = inv4(w.storeyWorld(storey));
  const P = (q) => { const r = apply4(Mi, q[0], q[1], 0); return [r[0], r[1]]; };
  return grids.map((g) => ({ ...g, axes: g.axes.map((x) => ({ ...x, a: P(x.a), b: P(x.b) })) }));
}

In detail

Method
Crossing of p + t r and q + u s: t = (q − p) × s ÷ (r × s).
Number types
64-bit numbers in millimeters.
Tolerances
Lines are parallel when |r × s| < 1e-6 × |r| × |s|. A crossing counts when t and u are within 1e-6 of [0, 1]. A grid is rectangular when unit directions are parallel or square within 1e-6. A line under 100 mm is refused. A line is seen end on when the cosine to the view direction is at least 0.866.
Checked by
tools/bimcraft-test/draw06-test.mjs calls gridLayout, gridSnap, gridsInView, readGrids and placeGrid in Node.

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