Writes a model's place on the map into the file: the map conversion numbers, the coordinate reference system and its length unit.
How the precision is kept
The conversion numbers (eastings, northings, height, the x axis direction and the scale) are written exactly as they are handed in, as 64-bit values with no rounding. Foot units are written from their legal definitions. The one rounded value is the site's latitude and longitude in the IFC2X3 form, which the format stores to a millionth of an arc second.
Calculations under this policy
5 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 transform from model units to map units, from the conversion's numbers.
(ax, ay) = (XAxisAbscissa, XAxisOrdinate) ÷ |…|; E = Eastings + s·(ax·x − ay·y); N = Northings + s·(ay·x + ax·y); H = Height + s·z
The x axis direction is normalized with Math.hypot, so a direction that is not unit length does not change the scale. A zero direction is divided by 1 instead of 0. Eastings and northings in the millions are added to scaled model coordinates, where a 64-bit number still resolves well below a micrometer.
georef-model.js
lines 19 to 25
BimCraft 0.8.1
/** model units → map units, column-major (the same as cc-ifc's georef; crs-georef.js conversionMatrix is the shared twin). */
export function convMatrix(c) {
let ax = c.XAxisAbscissa ?? 1, ay = c.XAxisOrdinate ?? 0;
const l = Math.hypot(ax, ay) || 1; ax /= l; ay /= l;
const s = c.Scale ?? 1;
return [ax * s, ay * s, 0, 0, -ay * s, ax * s, 0, 0, 0, 0, s, 0, c.Eastings, c.Northings, c.OrthogonalHeight, 1];
}
Meters per map unit
64-bit
The size of the map's length unit in meters, from its name.
US survey foot = 1200 ÷ 3937 m; foot = 0.3048 m; otherwise 1
Both foot values are written from their legal definitions: 1200 ÷ 3937 is computed in 64-bit and 0.3048 is the exact decimal. Each is the nearest 64-bit number to its definition.
georef-model.js
lines 26 to 27
BimCraft 0.8.1
/** metres per map unit from the label ePSet_ProjectedCRS.MapUnit carries. */
export const mapUnitMOf = (label) => (/us\s*survey/i.test(label) ? 1200 / 3937 : /^(foot|feet|ft)$/i.test(String(label || '').trim()) ? 0.3048 : 1);
Length unit of the map system
64-bit
Finds or makes the IFC unit for the map's coordinates.
wanted mm per unit: m 1000, ft 304.8, US survey ft 1200 ÷ 3937 × 1000; match when |unit − wanted| < 10⁻⁹ × wanted
The match is relative, scaled to the unit's size, so a survey foot and an international foot (2 parts in a million apart) are never mixed up. A new unit is written as its size in meters with no rounding.
georef-model.js
lines 46 to 65
BimCraft 0.8.1
/** The line of a length unit worth `metres` per unit, found or made. */
function mapUnit(w, unit) {
const want = { m: 1000, ft: 304.8, ftUS: 1200 / 3937 * 1000 }[unit] || 1000;
for (const cls of ['IFCSIUNIT', 'IFCCONVERSIONBASEDUNIT']) {
for (const id of ids(w.api, w.mid, cls)) {
const u = w.api.GetLine(w.mid, id, true);
if (val(u.UnitType) !== 'LENGTHUNIT') continue;
const mm = unitMm(u);
if (Math.abs(mm - want) < 1e-9 * want) return id;
}
}
const derived = { type: w.W.UNKNOWN, value: null, derived: true };
let si = null;
for (const id of ids(w.api, w.mid, 'IFCSIUNIT')) { const u = w.api.GetLine(w.mid, id, true); if (val(u.UnitType) === 'LENGTHUNIT' && !val(u.Prefix) && unitMm(u) === 1000) si = id; }
si ??= w.make('IFCSIUNIT', derived, w.E('LENGTHUNIT'), null, w.E('METRE'));
if (unit === 'm') return si;
const dim = w.make('IFCDIMENSIONALEXPONENTS', 1, 0, 0, 0, 0, 0, 0);
const mwu = w.make('IFCMEASUREWITHUNIT', w.T('IFCLENGTHMEASURE', want / 1000), w.H(si));
return w.make('IFCCONVERSIONBASEDUNIT', w.H(dim), w.E('LENGTHUNIT'), w.L(unit === 'ft' ? 'foot' : 'US survey foot'), w.H(mwu));
}
Latitude and longitude as degrees, minutes, seconds
64-bit
An angle in decimal degrees split into the four whole numbers the IFC site stores.
d = ⌊|deg|⌋; m = ⌊(|deg| − d) × 60⌋; s = ⌊rest × 60⌋; millionths = round((rest − s) × 10⁶)
The last part is rounded to a millionth of an arc second, about 0.03 mm on the ground. Each step multiplies a remainder by 60, which magnifies the rounding of the first subtraction. When the fraction rounds up to 1,000,000 it is stored as 1,000,000 and not carried into the seconds.
georef-model.js
lines 67 to 71
BimCraft 0.8.1
const compound = (deg) => {
const sgn = deg < 0 ? -1 : 1; let a = Math.abs(deg);
const d = Math.floor(a); a = (a - d) * 60; const m = Math.floor(a); a = (a - m) * 60; const s = Math.floor(a); const ms = Math.round((a - s) * 1e6);
return [sgn * d, sgn * m, sgn * s, sgn * ms];
};
Map conversion written to the file
64-bit
The six conversion numbers written into IfcMapConversion, new or in place.
Eastings, northings, height, the two axis numbers and the scale are written as they are handed in. Nothing is rounded or converted here.
IFC4 and later: IfcProjectedCRS and IfcMapConversion. IFC2X3: the property sets ePSet_MapConversion and ePSet_ProjectedCRS on the project, plus the site's latitude and longitude as degrees, minutes, seconds and millionths of a second.
Number types
64-bit numbers; the latitude and longitude parts are whole numbers.
Tolerances
A length unit already in the file is reused when it matches the wanted unit to 1e-9 of its size (relative).
Limits
The maths that produces the conversion numbers is in crs-georef.js, not in this file. In the latitude and longitude split, a fraction that rounds up to 1,000,000 millionths is not carried into the seconds.
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.