Turns the steel members, plates, holes, copes, bolts, grids and levels of an IFC file into a DetailCraft model.
How the precision is kept
Placements are composed in 64-bit in the file's own units and scaled to millimeters at the end. What is stored is then rounded: member ends, grid lines, levels, hole positions and bolt heads to 0.001 mm, rotations to 0.001 degree, hole and bolt diameters to 0.1 mm, cope sizes to 0.5 mm. A profile within 0.5 mm of a catalog shape on every dimension takes the catalog's dimensions.
Open item. Keep imported coordinates and rotations unrounded, or round them only where the file's own precision is known.
Calculations under this policy
14 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 SI prefixes are decimal constants (0.1, 0.01, 1e-3 …) multiplied by 1000 in 64-bit. A foot or inch file uses the conversion factor written in the file itself, so its accuracy is the file's.
ifc-import.js
lines 74 to 87
DetailCraft 0.8.1
let toMm = 1000, unitName = 'metre';
const prefix = { EXA: 1e18, PETA: 1e15, TERA: 1e12, GIGA: 1e9, MEGA: 1e6, KILO: 1e3, HECTO: 100, DECA: 10, DECI: 0.1, CENTI: 0.01, MILLI: 1e-3, MICRO: 1e-6, NANO: 1e-9 };
const siScale = (u) => (u && val(u.Name) === 'METRE' ? (u.Prefix ? prefix[val(u.Prefix)] ?? 1 : 1) : null);
for (const ua of ids(W.IFCUNITASSIGNMENT).slice(0, 1)) {
for (const r of line(ua).Units || []) {
const u = line(ref(r));
if (!u || val(u.UnitType) !== 'LENGTHUNIT') continue;
const s = siScale(u);
if (s !== null) { toMm = s * 1000; unitName = (u.Prefix ? String(val(u.Prefix)).toLowerCase() : '') + 'metre'; }
else if (u.ConversionFactor) {
const mwu = line(ref(u.ConversionFactor));
const f = Number(val(mwu?.ValueComponent));
const base = siScale(line(ref(mwu?.UnitComponent)));
if (Number.isFinite(f) && base !== null) { toMm = f * base * 1000; unitName = String(val(u.Name) || t('ifcimp.unit.converted')).toLowerCase(); }
A placement's axes
64-bit
The three axes and origin of an IFC axis placement.
Z = unit(Axis); X = unit(Ref − Z·(Ref·Z)); Y = Z × X
The reference direction is made square to the axis by subtracting its part along it, then normalized. If what is left is shorter than 0.5 a fallback direction is used.
ifc-import.js
lines 95 to 108
DetailCraft 0.8.1
const axis3 = (id) => {
const a = line(id);
if (!a) return I4();
const O = coords(ref(a.Location));
if (clsName(a) === 'IfcAxis2Placement2D') {
const X = a.RefDirection ? norm(dirOf(ref(a.RefDirection))) : [1, 0, 0];
return { X, Y: [-X[1], X[0], 0], Z: [0, 0, 1], O };
}
const Z = a.Axis ? norm(dirOf(ref(a.Axis))) : [0, 0, 1];
let X = a.RefDirection ? dirOf(ref(a.RefDirection)) : (Math.abs(Z[0]) > 0.9 ? [0, 0, 1] : [1, 0, 0]);
X = norm(sub(X, mul(Z, dot(X, Z))));
if (len(X) < 0.5) X = norm(cross(Math.abs(Z[2]) < 0.9 ? [0, 0, 1] : [0, 1, 0], Z));
return { X, Y: cross(Z, X), Z, O };
};
Placements combined
64-bit
One placement expressed inside another.
X′ = A·X, Y′ = A·Y, Z′ = A·Z, O′ = A·O + O_A
Sums of scaled axes in 64-bit. The axes are not made square again after each step, so a long chain of placements carries the rounding of every step; the result is checked later when the member's frame is rebuilt.
Recognizes a four-point outline as a rectangle and reads its center, axes and size.
square when |ê_i·ê_{i+1}| ≤ 1e-4; center = mean of the four points
A closing point within 1e-6 file units of the first is dropped. Width and height are the lengths of the first two edges times the unit factor.
ifc-import.js
lines 193 to 209
DetailCraft 0.8.1
/** A closed IfcPolyline / IfcIndexedPolyCurve that is a rectangle: { c, X, Y, w, h } (mm) or null. */
function rectangleOf(id) {
const cv = line(id);
if (!cv) return null;
let pts = [];
if (clsName(cv) === 'IfcPolyline') pts = (cv.Points || []).map((p) => coords(ref(p)));
else if (clsName(cv) === 'IfcIndexedPolyCurve' && !cv.Segments) {
const list = line(ref(cv.Points));
pts = (list?.CoordList || []).map((p) => p.map((v) => Number(val(v))).concat([0]).slice(0, 3));
} else return null;
if (pts.length > 1 && len(sub(pts[0], pts[pts.length - 1])) < 1e-6) pts.pop();
if (pts.length !== 4) return null;
const e = pts.map((p, i) => sub(pts[(i + 1) % 4], p));
for (let i = 0; i < 4; i++) if (Math.abs(dot(norm(e[i]), norm(e[(i + 1) % 4]))) > 1e-4) return null;
const c = mul(pts.reduce((s, p) => add(s, p), [0, 0, 0]), 0.25);
return { c, X: norm(e[0]), Y: norm(cross([0, 0, 1], norm(e[0]))), w: len(e[0]) * toMm, h: len(e[1]) * toMm }; // c in file units, like any placement
}
Matching a profile to the catalog
64-bit
Decides whether a profile from the file is one of the catalog shapes.
match when every |catalog − file| ≤ 0.5 mm
On a match the member gets the catalog shape, so each of its dimensions can differ from the file's by up to 0.5 mm. The fillet radius is not compared.
ifc-import.js
lines 211 to 227
DetailCraft 0.8.1
/** Catalog name for profile dims of a kind, or null. */
function catalogMatch(kind, d, pname) {
const fit = (p) => {
switch (kind) {
case 'I': return p.kind === 'I' && close(p.d, d.d) && close(p.bf, d.bf) && close(p.tw, d.tw) && close(p.tf, d.tf);
case 'C': return p.kind === 'C' && close(p.d, d.d) && close(p.bf, d.bf) && close(p.tw, d.tw) && close(p.tf, d.tf);
case 'L': return p.kind === 'L' && close(p.b, d.b) && close(p.t, d.t);
case 'RHS': return p.kind === 'RHS' && close(p.t, d.t) && ((close(p.h, d.h) && close(p.b, d.b)) || (close(p.h, d.b) && close(p.b, d.h)));
case 'CHS': return p.kind === 'CHS' && close(p.D, d.D) && close(p.t, d.t);
default: return false;
}
};
const byName = pname && CATALOG.get(pname.toUpperCase().replace(/\s+/g, ''));
if (byName && fit(byName)) return byName.name;
for (const p of CATALOG.values()) if (fit(p)) return p.name;
return null;
}
Reusing a custom profile
64-bit
Decides whether two profiles from the file are the same custom profile.
same when all dimensions agree after rounding to 0.01 mm
The lookup key rounds each dimension to two decimals with Math.round. Profiles that agree to that key share the first one's unrounded dimensions.
ifc-import.js
lines 476 to 489
DetailCraft 0.8.1
/** A custom profile for dims: reuses one with the same dims; name from the file (made unique). Returns its name or null. */
function custom(customs, pname, p, fallback) {
const key = JSON.stringify(Object.entries(p).map(([k, v]) => [k, typeof v === 'number' ? Math.round(v * 100) / 100 : v]));
if (customs.has(key)) return customs.get(key).profile.name;
let base = cleanText(pname || fallback, 34) || fallback;
let name = base, i = 2;
const taken = (nm) => CATALOG.has(nm.toUpperCase()) || /^PL\s*\d/i.test(nm) || [...customs.values()].some((c) => c.profile.name.toUpperCase() === nm.toUpperCase());
if (taken(name)) name = base + ' (file)';
while (taken(name)) name = base + ' (' + i++ + ')';
const r = cleanCustom({ ...p, name });
if (!r.ok) return null;
customs.set(key, r);
return r.profile.name;
}
Level heights
64-bit
The height of each storey, from its placement or its Elevation.
z = round(z × toMm × 1000) ÷ 1000
The height is rounded to 0.001 mm with Math.round before it is stored.
ifc-import.js
lines 243 to 247
DetailCraft 0.8.1
let z = M ? M.O[2] * toMm : null;
if ((z === null || (z === 0 && s.Elevation)) && s.Elevation !== null && s.Elevation !== undefined) z = Number(val(s.Elevation)) * toMm;
let nm = cleanText(val(s.Name) || '', 40) || t('ifcimp.level.default', { n: levels.length + 1 });
while (levels.some((l) => l.name === nm)) nm = nm.slice(0, 36) + ' (' + (levels.length + 1) + ')';
if (Number.isFinite(z)) levels.push({ name: nm, z: round3(z) });
Grid lines
64-bit
Which grid axes become numbered or lettered grid lines, and where.
x line when |d_y| > 0.9999; y line when |d_x| > 0.9999; position rounded to 0.001 mm
An axis up to about 0.8 degrees off X or Y is still taken as a grid line, at the coordinate of its first point. Lines closer than 0.5 mm to one already kept are dropped.
The start and end of a member from its extruded solid.
start = O × toMm; end = start + ext·depth
The origin is transformed in file units and then scaled. The extrusion must be square to the profile within 1e-4, as a cosine; a mirrored placement swaps start and end.
ifc-import.js
lines 303 to 311
DetailCraft 0.8.1
const O = mul(apply(Msolid, prof.pos.O), toMm); // the profile's origin (section center), mm
const ext = norm(applyDir(Msolid, dirOf(ref(b.solid.ExtrudedDirection))));
const depth = Number(val(b.solid.Depth)) * toMm;
let U = norm(Mprof.X), V = norm(Mprof.Y);
if (Math.abs(dot(ext, U)) > 1e-4 || Math.abs(dot(ext, V)) > 1e-4) { skip(cls, t('ifcimp.why.not-square')); continue; }
if (!(depth > 0)) { skip(cls, t('ifcimp.why.zero-length')); continue; }
// DetailCraft's frame: (u, v, x) right-handed; a mirrored placement is walked the other way
let start = O, end = add(O, mul(ext, depth));
if (dot(cross(U, V), ext) < 0) [start, end] = [end, start];
Plate from a rectangular solid
64-bit
The thickness, width and length of a plate from the three sizes of its box.
The rotation that puts the section the way the file has it, and the values stored for the member.
rot = atan2(U·v₀, U·u₀) × 180 ÷ π; stored: ends to 0.001 mm, rotation to 0.001°
After the rotation is found, the frame is rebuilt with it and must agree with the file's axes within 1e-4, else the member is left out. Start, end and rotation are then rounded to three decimals with Math.round before they are stored.
The face, position and diameter of each hole read from an opening.
d = round(2·r × 10) ÷ 10; x, y rounded to 0.001 mm
The hole's diameter is rounded to 0.1 mm, so a 17.46 mm hole (11/16 in) is stored as 17.5. A hole within 0.5 mm in position and 0.05 mm in diameter of one already there is not added twice.
ifc-import.js
lines 423 to 432
DetailCraft 0.8.1
const mid = add(e.O, mul(e.ext, e.depth / 2)), l = toLocal(f, mid, voff);
const k = pr.kind;
let face = null, y = 0;
if (Math.abs(ax) > 0.02) { skip(cls, t('ifcimp.why.hole-slanted')); continue; }
if (k === 'CHS') { skip(cls, t('ifcimp.why.hole-tube')); continue; }
if (Math.abs(au) > 0.98 && k !== 'PL') { face = 'web'; y = l.v; }
else if (Math.abs(av) > 0.98) { if (k === 'PL') face = 'plate'; else if (k === 'L') face = 'bottom'; else face = l.v > 0 ? 'top' : 'bottom'; y = l.u; }
if (!face) { skip(cls, t('ifcimp.why.hole-face')); continue; }
if (l.x < -0.5 || l.x > f.len + 0.5) { skip(cls, t('ifcimp.why.hole-off')); continue; }
const d = Math.round(2 * e.prof.r * 10) / 10, x = round3(l.x), yy = round3(y);
Copes from rectangular openings
64-bit
The length and depth of a cope read from a box-shaped opening.
Local placements composed as axes plus origin; the member axis is the extrusion; the rotation is found with Math.atan2 and then checked by rebuilding the frame.
Number types
64-bit numbers; stored values rounded as listed.
Tolerances
Catalog match 0.5 mm per dimension. Extrusion square to its profile within 1e-4 (cosine). Rebuilt axes within 1e-4. Grid lines parallel to X or Y within 0.9999 (cosine). Rectangle corners square within 1e-4. Top or bottom position within 0.01 mm. Holes: axis within 0.98 of a face normal and under 0.02 along the member.
Limits
Scaled mapped items, tapered and clipped solids are left out or simplified and said in the report. The unit factor is the product of the file's own conversion factor and a decimal prefix.
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.