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