Resources Numerical accuracy Calculation policy

IFC import: alignments, surfaces and units

Reads alignments, terrain surfaces and reference geometry from an IFC file into project coordinates and units.

How the precision is kept

File values are scaled to project units and moved through the file's map conversion in 64-bit. Alignment layouts are turned back into PIs by finding where each segment ends and crossing the tangents; transition curves are integrated with Simpson's rule in 400 steps, which sets the accuracy of those PIs. Terrain corners are joined when they agree to 6 decimals, and imported cant is rounded to a thousandth of a millimeter.

Calculations under this policy

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

ifc-civil-read.js CivilCraft 0.8.1 64-bit 13 calculations Reviewed

Tests bin/test-civilcraft-ifc.mjs, bin/test-civilcraft-native.mjs

File units to project units

64-bit

The factor that turns a length in the file into a length in the project.

k = file unit in meters ÷ (1 or 1200 ÷ 3937)

One division. A project in feet is always divided by the US survey foot 1200 ÷ 3937, so a file in international feet (0.3048) comes in scaled by 0.3048 ÷ (1200 ÷ 3937). When no project unit is given the file counts as metric if its unit is within 1e-9 of 1.

  • ifc-civil-read.js
  • line 167
  • CivilCraft 0.8.1
const fileUnit = g.lengthUnit, pu = units || (Math.abs(fileUnit - 1) < 1e-9 ? 'm' : 'ft'), k = fileUnit / M_PER[pu];   // file units → project units

File coordinates to project coordinates

64-bit

Moves a point through the file's map conversion, or adds the file's stored origin, and scales it to project units.

p′ = (M · p) × k or p′ = (p + origin) × k

A 3 × 4 matrix product in 64-bit followed by one multiplication by k. The matrix itself is built in ifc/geo-ctx.js.

  • ifc-civil-read.js
  • lines 175 to 179
  • CivilCraft 0.8.1
  toProject = (x, y, z = 0) => [(M[0] * x + M[4] * y + M[8] * z + M[12]) * k, (M[1] * x + M[5] * y + M[9] * z + M[13]) * k, (M[2] * x + M[6] * y + M[10] * z + M[14]) * k];
} else {
  const site = f.ofType('IFCSITE')[0], cc = site !== undefined ? P.get(site)?.CadCraft_Site : null;
  const ox = fin(cc?.OriginEasting) ? cc.OriginEasting : 0, oy = fin(cc?.OriginNorthing) ? cc.OriginNorthing : 0;
  toProject = (x, y, z = 0) => [(x + ox) * k, (y + oy) * k, z * k];

Where a horizontal segment ends

64-bit

The end point and end direction of a line, arc, cubic parabola or transition curve.

arc: center = start ∓ r·(sin, −cos)(dir), end at angle a₀ + L ÷ r; transition: x = ∫cos θ(u) du, y = ∫sin θ(u) du

Lines and arcs are closed form; the arc goes through its center and atan2. Transitions are integrated with Simpson's rule in 400 equal steps, so their end point carries the rule's truncation error rather than 64-bit rounding. A segment whose two curvatures differ by less than 1e-15 is treated as an arc.

  • ifc-civil-read.js
  • lines 45 to 68
  • CivilCraft 0.8.1
/** Where a horizontal segment ends: { x, y, dir }. r0 / r1 signed (+ = turns left), 0 = straight. */
export function segEnd(s) {
  const k0 = s.r0 ? 1 / s.r0 : 0, k1 = s.r1 ? 1 / s.r1 : 0;
  if (s.type === 'LINE' || (!k0 && !k1)) return { x: s.x + Math.cos(s.dir) * s.len, y: s.y + Math.sin(s.dir) * s.len, dir: s.dir };
  if (s.type === 'CIRCULARARC' || Math.abs(k0 - k1) < 1e-15) {
    const sg = Math.sign(k0), r = Math.abs(1 / k0), cx = s.x - sg * Math.sin(s.dir) * r, cy = s.y + sg * Math.cos(s.dir) * r, turn = s.len * k0;
    const a0 = Math.atan2(s.y - cy, s.x - cx);
    return { x: cx + Math.cos(a0 + turn) * r, y: cy + Math.sin(a0 + turn) * r, dir: s.dir + turn };
  }
  const kind = KIND_OF[s.type] || 'clothoid';
  if (kind === 'cubic') {
    // the cubic parabola y = x³ / 6RLp along its tangent (Lp from the length along the curve); out of a curve: walked back from its end
    const R = 1 / Math.max(Math.abs(k0), Math.abs(k1)), Lp = cubicParamFor(R, s.len), q = transitionAt('cubic', s.len, R, Lp);
    const rot = (a, u, v) => [u * Math.cos(a) - v * Math.sin(a), u * Math.sin(a) + v * Math.cos(a)];
    if (!k0) { const sg = Math.sign(k1), [dx, dy] = rot(s.dir, q.x, sg * q.y); return { x: s.x + dx, y: s.y + dy, dir: s.dir + sg * q.th }; }
    const sg = Math.sign(k0), d1 = s.dir + sg * q.th, [dx, dy] = rot(d1 + Math.PI, q.x, -sg * q.y);
    return { x: s.x - dx, y: s.y - dy, dir: d1 };
  }
  // a transition: curvature k0 + (k1 − k0)·f(u) (alignment.js shapes; the clothoid f(u) = u), heading dir + L·(k0·u + (k1 − k0)·F(u)); Simpson's rule
  const F = transitionShape(kind).F, n = 400, hs = s.len / n, th = (u) => s.dir + s.len * (k0 * (u / s.len) + (k1 - k0) * F(Math.min(1, u / s.len)));
  let x = 0, y = 0;
  for (let i = 0; i <= n; i++) { const wgt = i === 0 || i === n ? 1 : i % 2 ? 4 : 2, a = th(i * hs); x += wgt * Math.cos(a); y += wgt * Math.sin(a); }
  return { x: s.x + (x * hs) / 3, y: s.y + (y * hs) / 3, dir: th(s.len) };
}

Crossing of two tangents

64-bit

The PI: where the tangent into a curve meets the tangent out of it.

s = ((q − p) × v) ÷ (u × v); PI = p + u·s

The tangents are unit vectors from cos and sin of their directions. They count as parallel when the cross product is below 1e-12. For a small deflection the division by a small cross product magnifies rounding in the two points.

  • ifc-civil-read.js
  • lines 73 to 78
  • CivilCraft 0.8.1
function cross(p, d1, q, d2) {
  const ux = Math.cos(d1), uy = Math.sin(d1), vx = Math.cos(d2), vy = Math.sin(d2), den = ux * vy - uy * vx;
  if (Math.abs(den) < 1e-12) return null;
  const s = ((q.x - p.x) * vy - (q.y - p.y) * vx) / den;
  return { x: p.x + ux * s, y: p.y + uy * s };
}

Horizontal layout to PIs

64-bit

Turns a chain of lines, arcs and transitions into PIs with radius and spiral lengths.

Segments shorter than 1e-9 are ignored. Two straights make an angle point when their directions differ by more than 1e-9 rad. The arc and its spirals must agree on the radius within 1e-6 (absolute), otherwise the alignment is not made. The radius and spiral lengths are taken from the file values, not recomputed.

  • ifc-civil-read.js
  • lines 81 to 125
  • CivilCraft 0.8.1
export function layoutToPis(h0) {
  const h = (h0 || []).filter((s) => s.len > 1e-9);
  const notes = [];
  if (!h.length) return { pis: null, notes: [t('ifc.in.al.empty')] };
  const straight = (s) => s.type === 'LINE' || (!s.r0 && !s.r1);
  if (!straight(h[0]) || !straight(h[h.length - 1])) return { pis: null, notes: [t('ifc.in.al.endcurve')] };
  const pis = [{ x: h[0].x, y: h[0].y, r: 0 }];
  let i = 0;
  while (i < h.length) {
    const s = h[i];
    if (straight(s)) {
      const nx = h[i + 1];
      if (nx && straight(nx) && Math.abs(angDiff(nx.dir, s.dir)) > 1e-9) { const e = segEnd(s); pis.push({ x: e.x, y: e.y, r: 0 }); }   // an angle point
      i++;
      continue;
    }
    // a curve group: [clothoid 0 → R] [arc R] [clothoid R → 0], between two straights
    const before = h[i - 1], group = [];
    while (i < h.length && !straight(h[i])) group.push(h[i++]);
    const after = h[i];
    const types = group.map((g) => g.type).join(',');
    const TR = '(?:' + Object.keys(KIND_OF).join('|') + ')';
    const ok = new RegExp('^(' + TR + ',)?(CIRCULARARC)?(,' + TR + ')?$').test(types.replace(/^,|,$/g, ''));
    let R = 0, ls1 = 0, ls2 = 0, bad = !ok || !before || !after, kind = null;
    const lenOf = (g, r) => (KIND_OF[g.type] === 'cubic' ? cubicParamFor(r, g.len) : g.len);   // the cubic's PI length is along its tangent
    for (const g of group) {
      if (g.type === 'CIRCULARARC') { if (R && Math.abs(Math.abs(g.r0) - R) > 1e-6) bad = true; R = Math.abs(g.r0); }
      else if (KIND_OF[g.type]) {
        if (kind && kind !== KIND_OF[g.type]) notes.push(t('ifc.in.al.mixedtr'));
        kind = kind || KIND_OF[g.type];
        if (!g.r0 && g.r1) { if (ls1 || group.indexOf(g) !== 0) bad = true; R = R || Math.abs(g.r1); ls1 = lenOf(g, R); if (Math.abs(Math.abs(g.r1) - R) > 1e-6) bad = true; }
        else if (g.r0 && !g.r1) { if (group.indexOf(g) !== group.length - 1) bad = true; R = R || Math.abs(g.r0); ls2 = lenOf(g, R); if (Math.abs(Math.abs(g.r0) - R) > 1e-6) bad = true; }
        else bad = true;   // a spiral between two curves
      } else bad = true;
    }
    if (bad || !(R > 0)) return { pis: null, notes: [t('ifc.in.al.compound')] };
    const last = group[group.length - 1], e = segEnd(last);
    const pi = cross({ x: group[0].x, y: group[0].y }, group[0].dir, { x: e.x, y: e.y }, e.dir);
    if (!pi) return { pis: null, notes: [t('ifc.in.al.compound')] };
    pis.push({ x: pi.x, y: pi.y, r: R, ...(ls1 ? { ls1 } : {}), ...(ls2 ? { ls2 } : {}), ...(kind && kind !== 'clothoid' ? { tr: kind } : {}) });
  }
  const e = segEnd(h[h.length - 1]);
  pis.push({ x: e.x, y: e.y, r: 0 });
  return { pis, notes };
}

Vertical layout to PVIs

64-bit

Turns grades and vertical curves into PVIs with station, elevation and curve length.

curve: sta = d₀ + L ÷ 2, z = z₀ + g₀·L ÷ 2; end: z = z₀ + (g₀ + g₁) ÷ 2 × L

A curve's PVI is placed at mid-length on the entry grade, which is the crossing of the grades for a symmetric parabola. Two grades make a break when they differ by more than 1e-12. Circular and clothoid vertical segments are read the same way and noted.

  • ifc-civil-read.js
  • lines 127 to 142
  • CivilCraft 0.8.1
export function layoutToPvis(v0, start = 0) {
  const v = (v0 || []).filter((s) => s.len > 1e-9).sort((a, b) => a.d0 - b.d0);
  const notes = [];
  if (!v.length) return { pvis: [], notes };
  const pvis = [{ sta: start + v[0].d0, z: v[0].z0, L: 0 }];
  v.forEach((s, i) => {
    if (s.type === 'CIRCULARARC') notes.push(t('ifc.in.al.vcircle'));
    if (s.type === 'CLOTHOID') notes.push(t('ifc.in.al.vclothoid'));
    if (s.type !== 'CONSTANTGRADIENT') { pvis.push({ sta: start + s.d0 + s.len / 2, z: s.z0 + (s.g0 * s.len) / 2, L: s.len }); return; }
    const nx = v[i + 1];
    if (nx && nx.type === 'CONSTANTGRADIENT' && Math.abs(nx.g0 - s.g0) > 1e-12) pvis.push({ sta: start + s.d0 + s.len, z: s.z0 + s.g0 * s.len, L: 0 });
  });
  const l = v[v.length - 1], zEnd = l.type === 'CONSTANTGRADIENT' ? l.z0 + l.g0 * l.len : l.z0 + ((l.g0 + l.g1) / 2) * l.len;
  pvis.push({ sta: start + l.d0 + l.len, z: zEnd, L: 0 });
  return { pvis, notes: [...new Set(notes)] };
}

Horizontal segment values

64-bit

Reads a segment's start point, direction, radii and length into project units.

length × (map scale × k); direction × angle unit + rotation

Radii and lengths are multiplied by the map scale times the unit factor; the direction by the file's angle unit plus the map rotation. Nothing is rounded.

  • ifc-civil-read.js
  • lines 233 to 234
  • CivilCraft 0.8.1
const rr = (a) => { const r = g.num(g.a(dp, a), 0); return r * LEN; };
h.push({ type: g.enm(g.a(dp, 'PredefinedType')), x, y, dir: g.num(g.a(dp, 'StartDirection'), 0) * g.angleUnit + rot, r0: rr('StartRadiusOfCurvature'), r1: rr('EndRadiusOfCurvature'), len: g.num(g.a(dp, 'SegmentLength'), 0) * LEN });

Vertical segment values

64-bit

Reads a vertical segment's start distance, length, height and gradients.

z₀ = (height × map scale + map height) × k

The start height is scaled by the map scale, shifted by the map conversion's height, then scaled to project units. Gradients are copied.

  • ifc-civil-read.js
  • line 237
  • CivilCraft 0.8.1
v.push({ type: g.enm(g.a(dp, 'PredefinedType')), d0: g.num(g.a(dp, 'StartDistAlong'), 0) * LEN, len: g.num(g.a(dp, 'HorizontalLength'), 0) * LEN, z0: (g.num(g.a(dp, 'StartHeight'), 0) * sc + (geo?.height || 0)) * k, g0: g.num(g.a(dp, 'StartGradient'), 0), g1: g.num(g.a(dp, 'EndGradient'), 0) });

IFC 4x1 vertical curves

64-bit

Reads an IFC 4x1 vertical segment and works out the end grade of a curve from its radius.

g₁ = g₀ ± L ÷ R_v

The end grade is the start grade plus or minus length over the vertical radius. The start distance has the alignment's start station subtracted.

  • ifc-civil-read.js
  • lines 269 to 271
  • CivilCraft 0.8.1
const d0 = g.num(q[3], 0) * LEN - start, len = g.num(q[4], 0) * LEN, z0 = (g.num(q[5], 0) * sc + (geo?.height || 0)) * k, g0 = g.num(q[6], 0);
if (tp === 'IFCALIGNMENT2DVERSEGLINE') v.push({ type: 'CONSTANTGRADIENT', d0, len, z0, g0, g1: g0 });
else { const conv = g.bool(q[8], false), Rv = g.num(q[7], 1) * LEN; v.push({ type: tp === 'IFCALIGNMENT2DVERSEGPARABOLICARC' ? 'PARABOLICARC' : 'CIRCULARARC', d0, len, z0, g0, g1: g0 + (conv ? -1 : 1) * (len / Rv) }); }

Applied cant of a curve

64-bit

The cant of each curve from the constant-cant segment over its middle.

E = right − left (or left − right); stored = round(E × 1000) ÷ 1000

The cant in millimeters is rounded to 3 decimals before it is stored on the PI, and the rail head distance is rounded the same way. The segment is matched to the curve's middle within 1e-6, and a cant below 1e-6 mm is not stored.

  • ifc-civil-read.js
  • lines 202 to 207
  • CivilCraft 0.8.1
  const mid = (c.sc + c.cs) / 2 - start, sg = cant.segs.find((q) => q.type === 'CONSTANTCANT' && q.d0 <= mid + 1e-6 && mid <= q.d0 + q.len + 1e-6);
  if (sg) { const E = c.ccw ? sg.sr - sg.sl : sg.sl - sg.sr; if (Math.abs(E) > 1e-6) r.pis[c.piIndex].cant = Math.round(E * 1000) / 1000; }
}
let own = null;
try { own = JSON.parse(P.get(id)?.CadCraft_Alignment?.RailSettings || 'null'); } catch { own = null; }
rail = cleanRail(own || { ...railDefaults(pu), base: Math.round(cant.base * 1000) / 1000 }, pu);

CivilCraft's own PI and PVI tables

64-bit

Reads the PI and PVI tables CivilCraft wrote into an IFC4 or IFC2x3 file.

Each number is parsed from text and multiplied by the unit factor. The text was written with 6 decimals by the export, so the alignment comes back rounded to that.

  • ifc-civil-read.js
  • lines 281 to 282
  • CivilCraft 0.8.1
const pis = c.PIs.split(';').filter(Boolean).map((row) => row.split(',')).map(([x, y, r, a, b, tr, ca]) => ({ x: +x * k, y: +y * k, r: (+r || 0) * k, ...(+a ? { ls1: +a * k } : {}), ...(+b ? { ls2: +b * k } : {}), ...(tr && tr !== 'clothoid' ? { tr } : {}), ...(+ca ? { cant: +ca } : {}) }));   // tr, cant: 71.26 / B
const pvis = (c.PVIs || '').split(';').filter(Boolean).map(num).map(([sta, z, L]) => ({ sta: sta * k, z: z * k, L: (L || 0) * k }));

Element geometry into project coordinates

64-bit

Moves each mesh vertex through its placement and then into project coordinates.

The placement matrix product and the map conversion are done in 64-bit on the positions that ifc/geometry.js returns. Reference elements are stored in a Float64Array.

  • ifc-civil-read.js
  • lines 310 to 312
  • CivilCraft 0.8.1
const x = m.pos[i], y = m.pos[i + 1], z = m.pos[i + 2];
const w = [T[0] * x + T[4] * y + T[8] * z + T[12], T[1] * x + T[5] * y + T[9] * z + T[13], T[2] * x + T[6] * y + T[10] * z + T[14]];
pos.push(...toProject(w[0], w[1], w[2]));

Terrain corners joined into points

64-bit

Turns a terrain's separate triangles into one point list and faces.

Corners are matched on x, y and z rounded to 6 decimals with toFixed(6). The first corner seen keeps its full coordinates; a later corner that matches takes the first one's position, a move of up to about 1e-6 project units. Triangles that collapse are dropped.

  • ifc-civil-read.js
  • line 321
  • CivilCraft 0.8.1
const vid = (i) => { const key = pos[i * 3].toFixed(6) + ',' + pos[i * 3 + 1].toFixed(6) + ',' + pos[i * 3 + 2].toFixed(6); let j = at.get(key); if (j === undefined) { j = pts.length; pts.push({ x: pos[i * 3], y: pos[i * 3 + 1], z: pos[i * 3 + 2] }); at.set(key, j); } return j; };

In detail

Method
Segment end points in closed form for lines and arcs, Simpson's rule (n = 400) for transitions; PI = crossing of the tangents before and after a curve; PVI = crossing of the grades.
Number types
64-bit numbers; reference meshes kept in Float64Array.
Tolerances
A file is metric when its unit is within 1e-9 of 1 m. Segments shorter than 1e-9 ignored. Angle point when directions differ by more than 1e-9 rad. Radii in one curve must agree within 1e-6. Tangents parallel when |sin of the angle between| < 1e-12. Grade break when grades differ by more than 1e-12. Terrain weld at 6 decimals. Cant and rail head distance rounded to 0.001 mm.
Limits
A feet project is taken as US survey feet (1200 ÷ 3937 m) for every file. A circular vertical arc is read as a parabola of the same length and grades. CivilCraft's own PI table in an IFC4 or IFC2x3 file was written with 6 decimals.

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