Adds up the quantities and number properties of IFC elements by group, lists rooms with their areas and volumes, and decides whether a property changed between two versions.
How the precision is kept
Property and quantity values are read from the file as 64-bit numbers, but the property index rounds each one to 6 decimals before it is kept. Totals add those values in element order and are then cut to 12 significant digits to hide binary noise. Two versions of a number count as the same within a relative 1e-9.
Open item. Keep property numbers as read, without the rounding to 6 decimals, and round only where a value is shown.
Calculations under this policy
6 calculations in 4 source files. 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.
Turns an IFC property or quantity value into the number or text the index, the totals and the comparison use.
number kept = round(x × 10⁶) ÷ 10⁶
Every finite number is rounded to 6 decimals with Math.round before it is stored in the index, whatever its unit or size. This is the value that totals, schedules, the CSV exports and the model comparison then read.
ifc-props.js
lines 45 to 54
BimCraft 0.8.1
function plain(v) {
if (v === null || v === undefined) return undefined;
if (Array.isArray(v)) { const a = v.map(plain).filter((x) => x !== undefined); return a.length ? a.join(', ') : undefined; }
let x = typeof v === 'object' && 'value' in v ? v.value : v;
if (x === null || x === undefined || x === '') return undefined;
if (typeof x === 'number') return Number.isFinite(x) ? Math.round(x * 1e6) / 1e6 : undefined;
if (x === true || x === '.T.' || x === 'T') return 'Yes';
if (x === false || x === '.F.' || x === 'F') return 'No';
return String(x);
}
Counts elements and sums the chosen number fields for each combination of group values and for the whole selection.
sum = Σ vᵢ over the elements with a number; counted = how many had one
Values are added one by one in element order, with no compensated summation, then passed through tidy. The group total and the grand total are accumulated separately from the same values.
ifc-qto.js
lines 36 to 55
BimCraft 0.8.1
export function totals(ix, { keys = null, groupBy = ['cls'], sum = [] } = {}) {
const kb = ix.keyBase;
const known = keys ? new Set(ix.ids) : null;
const ids = keys ? [...new Set([...keys].map((k) => k - kb))].filter((id) => known.has(id)) : ix.ids;
const gv = groupBy.slice(0, MAX_GROUP).map((f) => ix.values(f)), sv = sum.map((f) => ix.values(f));
const rows = new Map();
const total = { count: 0, sums: sum.map(() => 0), counted: sum.map(() => 0) };
for (const id of ids) {
const values = gv.map((m) => { const v = m.get(id); return v === undefined ? '' : v; });
const k = JSON.stringify(values);
let r = rows.get(k);
if (!r) rows.set(k, (r = { values, count: 0, sums: sum.map(() => 0), counted: sum.map(() => 0) }));
r.count++; total.count++;
sv.forEach((m, i) => { const v = m.get(id); if (typeof v === 'number' && Number.isFinite(v)) { r.sums[i] += v; r.counted[i]++; total.sums[i] += v; total.counted[i]++; } });
}
const out = [...rows.values()].sort((a, b) => { for (let i = 0; i < a.values.length; i++) { const c = cmp(a.values[i], b.values[i]); if (c) return c; } return 0; });
for (const r of out) r.sums = r.sums.map(tidy);
total.sums = total.sums.map(tidy);
return { groupBy: groupBy.slice(0, MAX_GROUP), sum: [...sum], rows: out, total };
}
Spaces schedule totals
64-bit
The total floor area and volume of the listed rooms.
Adds the rows' area and volume in sorted row order and rounds the result to 12 significant digits with tidy. Rows without a number add zero.
Decides whether a number or text property changed between two versions of a model.
same when |a − b| ≤ 1e-9 × max(1, |a|, |b|)
Numbers compare with a tolerance that is relative above 1 and absolute (1e-9) below it; text compares exactly. Both values were already rounded to 6 decimals when the index read them.
ifc-compare.js
line 44
BimCraft 0.8.1
const sameVal = (a, b) => (typeof a === 'number' && typeof b === 'number' ? Math.abs(a - b) <= 1e-9 * Math.max(1, Math.abs(a), Math.abs(b)) : a === b);
How a property or quantity value from the file is written out in the Properties panel.
shown = round(x × 10⁴) ÷ 10⁴
A whole number is shown as it is; any other number is rounded to 4 decimals with Math.round before it is turned into text. The value in the file is not changed by this.
ifc-model.js
lines 287 to 295
BimCraft 0.8.1
function fmt(v) {
if (Array.isArray(v)) return v.map(fmt).join(', ');
const x = val(v);
if (x === null || x === undefined) return '';
if (typeof x === 'number') return Number.isInteger(x) ? String(x) : String(Math.round(x * 10000) / 10000);
if (x === true || x === '.T.' || x === 'T' || x === 'true') return 'Yes';
if (x === false || x === '.F.' || x === 'F' || x === 'false') return 'No';
return String(x);
}
In detail
Method
Plain sums per group and for the whole selection; an element with no number for a column adds nothing and is counted as left out.
Number types
64-bit numbers in JavaScript Maps and arrays.
Tolerances
Values rounded to 1e-6 (absolute) on reading. Totals rounded to 12 significant digits. Comparison: |a − b| ≤ 1e-9 × max(1, |a|, |b|).
Checked by
bin/test-bimcraft.mjs runs totals, totalsCsv and spaceSchedule (--qto) and compareModels (--compare) in Node.
Limits
A value smaller than 0.0000005 in its own unit becomes zero in the index. Totals carry 12 significant digits, not 15.
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.