Resources Numerical accuracy Calculation policy

IFC units and placements

Reads the length and angle units of an IFC file and turns its placements, transformation operators, grid placements and map conversion into 4 × 4 matrices.

How the precision is kept

Numbers are taken from the file as 64-bit values and stay 64-bit. Unit factors are powers of ten for SI prefixes, the file's own conversion factor for other units, and the exact 0.0254 and 0.3048 when a unit is known only by name. A placement chain is the product of its local matrices, in the file's length unit, so no unit rounding enters the geometry.

Calculations under this policy

13 calculations in 2 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.

ifc/geo-ctx.js BimCraft 0.8.1 64-bit 7 calculations Reviewed

Tests tools/ifc-test/test.mjs

SI value of a file unit

64-bit

Meters per file length unit and radians per file angle unit.

SI unit: 10^prefix; conversion-based unit: factor × factor of its base unit; by name: degree π ÷ 180, inch 0.0254, foot 0.3048

The prefix factors are powers of ten and the inch and foot factors are exact. A conversion factor in the file is multiplied in as written. The name fallback is used only when the unit carries no usable factor, and it returns the length factor for any unit whose name contains INCH, FOOT or FEET.

  • ifc/geo-ctx.js
  • lines 78 to 99
  • BimCraft 0.8.1
/** The SI value of one of `u` (SI base unit with prefix, or a conversion-based unit through its factor). */
unitFactor(u, base, depth = 0) {
  if (depth > 8 || u === null) return null;
  const t = this.t(u);
  if (t === 'IFCSIUNIT') {
    const p = this.enm(this.a(u, 'Prefix'));
    return p ? PREFIX[p] ?? 1 : 1;
  }
  if (t === 'IFCCONVERSIONBASEDUNIT' || t === 'IFCCONVERSIONBASEDUNITWITHOFFSET') {
    const m = this.ref(this.a(u, 'ConversionFactor'));
    if (m !== null) {
      const v = this.num(this.a(m, 'ValueComponent'));
      const inner = this.unitFactor(this.ref(this.a(m, 'UnitComponent')), base, depth + 1) ?? 1;
      if (v && Number.isFinite(v)) return v * inner;
    }
    const n = String(this.str(this.a(u, 'Name')) || '').toUpperCase();
    if (base === 'RADIAN' && n.includes('DEGREE')) return Math.PI / 180;
    if (n.includes('INCH')) return 0.0254;
    if (n.includes('FOOT') || n.includes('FEET')) return 0.3048;
  }
  return null;
}

Axis placement as a matrix

64-bit

The matrix of an IfcAxis2Placement3D, 2D or IfcAxis1Placement.

columns = (x, z × x, z, origin), x = reference direction projected square to z

Directions are normalized when read, X is projected into the plane normal to Z and normalized again, and Y is their cross product. The origin is copied unchanged.

  • ifc/geo-ctx.js
  • lines 125 to 149
  • BimCraft 0.8.1
/** IfcAxis2Placement3D / 2D / IfcAxis1Placement → matrix (2D placements lie in the XY plane). */
axis2(id) {
  if (id === null) return I4();
  const t = this.t(id);
  const o = this.point(this.ref(this.a(id, 'Location')));
  if (t === 'IFCAXIS2PLACEMENT2D') {
    let x = this.dir(this.ref(this.a(id, 'RefDirection')), [1, 0, 0]);
    x = norm([x[0], x[1], 0]);
    if (!x[0] && !x[1]) x = [1, 0, 0];
    return frame([o[0], o[1], 0], x, [-x[1], x[0], 0], [0, 0, 1]);
  }
  if (t === 'IFCAXIS1PLACEMENT') {
    const z = this.dir(this.ref(this.a(id, 'Axis')), [0, 0, 1]);
    const x = perp(z);
    return frame(o, x, cross(z, x), z);
  }
  if (t === 'IFCAXIS2PLACEMENT3D') {
    const z = this.dir(this.ref(this.a(id, 'Axis')), [0, 0, 1]);
    let x = this.dir(this.ref(this.a(id, 'RefDirection')), null);
    x = firstProjAxis(z, x);
    return frame(o, x, cross(z, x), z);
  }
  if (t === 'IFCAXIS2PLACEMENTLINEAR') { this.note('Linear placements are drawn at their start point'); return I4(); }
  return I4();
}

First projection axis

64-bit

The X axis of a placement: the reference direction projected into the plane normal to Z.

x = normalize(v − z (v · z))

When the reference direction is nearly parallel to Z the projection subtracts nearly equal vectors and the remaining direction loses digits; a projection of exactly zero falls back to any perpendicular.

  • ifc/geo-ctx.js
  • lines 283 to 291
  • BimCraft 0.8.1
/** IFC IfcFirstProjAxis: X projected into the plane normal to Z (default (1,0,0), or (0,1,0) when Z is X). */
export function firstProjAxis(z, x) {
  let v = x;
  if (!v) v = Math.abs(z[0]) > 0.999999 && Math.abs(z[1]) < 1e-6 && Math.abs(z[2]) < 1e-6 ? [0, 1, 0] : [1, 0, 0];
  let p = sub(v, scale(z, dot(v, z)));
  let n = norm(p);
  if (!n[0] && !n[1] && !n[2]) n = perp(z);
  return n;
}

Cartesian transformation operator

64-bit

The matrix of an IfcCartesianTransformationOperator, uniform or not, in 2D or 3D.

columns = (x s₁, y s₂, z s₃, origin)

The axes are orthogonalized in order (Z, then X against Z, then Y against both) and each is multiplied by its own scale. Missing scales default to the first scale.

  • ifc/geo-ctx.js
  • lines 152 to 174
  • BimCraft 0.8.1
/** IfcCartesianTransformationOperator2D/3D (+ NonUniform): IFC4 IfcBaseAxis / IfcFirstProjAxis / IfcSecondProjAxis. */
operator(id) {
  if (id === null) return I4();
  const t = this.t(id);
  const two = t === 'IFCCARTESIANTRANSFORMATIONOPERATOR2D' || t === 'IFCCARTESIANTRANSFORMATIONOPERATOR2DNONUNIFORM';
  const o = this.point(this.ref(this.a(id, 'LocalOrigin')));
  const s1 = this.num(this.a(id, 'Scale'), 1);
  const s2 = this.num(this.a(id, 'Scale2'), s1);
  const s3 = this.num(this.a(id, 'Scale3'), s1);
  const a1 = this.dir(this.ref(this.a(id, 'Axis1')), null);
  const a2 = this.dir(this.ref(this.a(id, 'Axis2')), null);
  if (two) {
    let x = a1 ? norm([a1[0], a1[1], 0]) : [1, 0, 0];
    let y = a2 ? norm([a2[0], a2[1], 0]) : [-x[1], x[0], 0];
    return [x[0] * s1, x[1] * s1, 0, 0, y[0] * s2, y[1] * s2, 0, 0, 0, 0, 1, 0, o[0], o[1], 0, 1];
  }
  const z = this.dir(this.ref(this.a(id, 'Axis3')), [0, 0, 1]);
  const x = firstProjAxis(z, a1);
  let y;
  if (a2) { const v = sub(a2, scale(z, dot(a2, z))); y = norm(sub(v, scale(x, dot(v, x)))); if (!y[0] && !y[1] && !y[2]) y = cross(z, x); }
  else y = cross(z, x);
  return [x[0] * s1, x[1] * s1, x[2] * s1, 0, y[0] * s2, y[1] * s2, y[2] * s2, 0, z[0] * s3, z[1] * s3, z[2] * s3, 0, o[0], o[1], o[2], 1];
}

World matrix of a placement

64-bit

The position of an element in the model from its chain of relative placements.

world = parent world × local

Each level is one 4 × 4 product in 64-bit, cached per placement; a chain deeper than 64 stops with a note. Translations are carried at full size in the file's length unit, so a model placed at large map coordinates keeps those large numbers in every matrix.

  • ifc/geo-ctx.js
  • lines 176 to 205
  • BimCraft 0.8.1
/** World matrix of an IfcObjectPlacement (IfcLocalPlacement chain, IfcGridPlacement). Cached; cycles stop. */
placement(id, depth = 0) {
  if (id === null || id === undefined) return I4();
  const hit = this.places.get(id);
  if (hit) return hit;
  if (depth > 64) { this.note('A placement chain loops'); return I4(); }
  const t = this.t(id);
  let m = I4();
  if (t === 'IFCLOCALPLACEMENT') {
    const rel = this.ref(this.a(id, 'PlacementRelTo'));
    const local = this.axis2(this.ref(this.a(id, 'RelativePlacement')));
    m = rel !== null ? mul(this.placement(rel, depth + 1), local) : local;
  } else if (t === 'IFCGRIDPLACEMENT') {
    m = this.gridPlacement(id);
  } else if (t === 'IFCLINEARPLACEMENT' && this.t(this.ref(this.a(id, 'RelativePlacement'))) === 'IFCAXIS2PLACEMENTLINEAR' && linearPlacement(this, this.ref(this.a(id, 'RelativePlacement')))) {
    // a station along an alignment, with its offsets (the alignment curve is in the frame PlacementRelTo gives)
    const rel = this.ref(this.a(id, 'PlacementRelTo'));
    const local = linearPlacement(this, this.ref(this.a(id, 'RelativePlacement')));
    m = rel !== null ? mul(this.placement(rel, depth + 1), local) : local;
  } else if (t && this.s.entity(t)?.index.has('CartesianPosition')) {
    // IFC4X3 linear placements carry their position as a plain placement too
    const cp = this.ref(this.a(id, 'CartesianPosition'));
    const rel = this.ref(this.a(id, 'PlacementRelTo'));
    const local = cp !== null ? this.axis2(cp) : I4();
    m = rel !== null ? mul(this.placement(rel, depth + 1), local) : local;
    if (cp === null) this.note('A linear placement without a Cartesian position is drawn at the origin');
  } else if (t) this.note(this.s.pretty(t) + ' placements are not read yet; drawn at the origin');
  this.places.set(id, m);
  return m;
}

Grid placement

64-bit

The point where two grid axes meet, with their offsets, as a placement.

t = ((pB − pA) × dB) ÷ (dA × dB); point = pA + dA t

Each axis is taken as the straight line from its first to its last point. The division is guarded: a denominator under 1e-12 (unit directions, so the sine of the angle between them) gives the identity placement. Nearly parallel axes above that limit give a point with few good digits.

  • ifc/geo-ctx.js
  • lines 207 to 252
  • BimCraft 0.8.1
/**
 * IfcGridPlacement: the intersection of two grid axes (straight axis curves) offset by IfcVirtualGridIntersection,
 * in the coordinate system of the IfcGrid the axes belong to (its ObjectPlacement; else PlacementRelTo).
 */
gridPlacement(id) {
  const vgi = this.ref(this.a(id, 'PlacementLocation'));
  if (vgi === null) return I4();
  const axes = this.refs(this.a(vgi, 'IntersectingAxes'));
  const offs = (this.a(vgi, 'OffsetDistances') || []).map((v) => this.num(v, 0));
  if (axes.length < 2) return I4();
  const line = (ax) => {
    const c = this.ref(this.a(ax, 'AxisCurve'));
    const ct = this.t(c);
    let pts = null;
    if (ct === 'IFCPOLYLINE') pts = this.refs(this.a(c, 'Points')).map((p) => this.point(p));
    else if (ct === 'IFCLINE') { const o = this.point(this.ref(this.a(c, 'Pnt'))); const v = this.vector(this.ref(this.a(c, 'Dir'))); pts = [o, [o[0] + v[0], o[1] + v[1], o[2] + v[2]]]; }
    if (!pts || pts.length < 2) return null;
    let d = norm(sub(pts[pts.length - 1], pts[0]));
    if (this.bool(this.a(ax, 'SameSense'), true) === false) d = scale(d, -1);
    return { p: pts[0], d };
  };
  const A = line(axes[0]), B = line(axes[1]);
  if (!A || !B) { this.note('Grid placements on curved axes are drawn at the origin'); return I4(); }
  // offsets move each axis to its left (seen along the axis), the third one up
  const nA = [-A.d[1], A.d[0], 0], nB = [-B.d[1], B.d[0], 0];
  const pA = [A.p[0] + nA[0] * (offs[0] || 0), A.p[1] + nA[1] * (offs[0] || 0)];
  const pB = [B.p[0] + nB[0] * (offs[1] || 0), B.p[1] + nB[1] * (offs[1] || 0)];
  const den = A.d[0] * B.d[1] - A.d[1] * B.d[0];
  if (Math.abs(den) < 1e-12) return I4();
  const tA = ((pB[0] - pA[0]) * B.d[1] - (pB[1] - pA[1]) * B.d[0]) / den;
  const o = [pA[0] + A.d[0] * tA, pA[1] + A.d[1] * tA, offs[2] || 0];
  const refDir = this.ref(this.a(id, 'PlacementRefDirection'));
  let x = A.d;
  if (refDir !== null && this.t(refDir) === 'IFCDIRECTION') x = this.dir(refDir, A.d);
  else if (refDir !== null && this.t(refDir) === 'IFCVIRTUALGRIDINTERSECTION') this.note('Grid placements aimed at another grid point face the first axis');
  x = norm([x[0], x[1], 0]);
  const local = frame(o, x, [-x[1], x[0], 0], [0, 0, 1]);
  // the grid's own placement
  let base = null;
  for (const attr of ['UAxes', 'VAxes', 'WAxes']) {
    const grids = this.f.inverse('IFCGRID', attr).get(axes[0]);
    if (grids && grids.length) { base = this.ref(this.a(grids[0], 'ObjectPlacement')); break; }
  }
  if (base === null) base = this.ref(this.a(id, 'PlacementRelTo'));
  return base !== null && base !== id ? mul(this.placement(base, 1), local) : local;
}

Map conversion

64-bit

The matrix that takes model coordinates to map coordinates, with its rotation and scale.

map = (E, N, H) + R(θ) · S · model; θ = atan2(ordinate, abscissa)

The X axis abscissa and ordinate are normalized with Math.hypot before they are used; a zero length is replaced by 1. Eastings and northings are added at full size, so the map coordinates keep about 15 to 16 digits in total, shared between the large offset and the detail.

  • ifc/geo-ctx.js
  • lines 256 to 281
  • BimCraft 0.8.1
GeoCtx.prototype.georef = function georef() {
  const f = this.f;
  const op = f.ofType('IFCMAPCONVERSION', true)[0] ?? f.ofType('IFCRIGIDOPERATION', true)[0];
  if (op === undefined) return null;
  const t = this.t(op);
  const crsId = this.ref(this.a(op, 'TargetCRS'));
  const crs = crsId === null ? null : {
    name: this.str(this.a(crsId, 'Name')), description: this.str(this.a(crsId, 'Description')),
    geodeticDatum: this.str(this.a(crsId, 'GeodeticDatum')), verticalDatum: this.str(this.a(crsId, 'VerticalDatum')),
    projection: this.str(this.a(crsId, 'MapProjection')), zone: this.str(this.a(crsId, 'MapZone')),
  };
  if (t === 'IFCRIGIDOPERATION') {
    const e = this.num(this.a(op, 'FirstCoordinate'), 0), n = this.num(this.a(op, 'SecondCoordinate'), 0), h = this.num(this.a(op, 'Height'), 0);
    return { kind: 'rigid', crs, eastings: e, northings: n, height: h, xAxis: [1, 0], rotation: 0, scale: 1, matrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, e, n, h, 1] };
  }
  const e = this.num(this.a(op, 'Eastings'), 0), n = this.num(this.a(op, 'Northings'), 0), h = this.num(this.a(op, 'OrthogonalHeight'), 0);
  let ax = this.num(this.a(op, 'XAxisAbscissa'), 1), ay = this.num(this.a(op, 'XAxisOrdinate'), 0);
  const l = Math.hypot(ax, ay) || 1; ax /= l; ay /= l;
  const s = this.num(this.a(op, 'Scale'), 1);
  // Scale is the unit difference between the model and the map (IFC4), so heights take it too: a millimetre model with
  // Scale 0.001 has heights in metres on the map. Only IfcMapConversionScaled (IFC4X3) names its own FactorY / FactorZ.
  const sy = this.num(this.a(op, 'FactorY'), s), sz = this.num(this.a(op, 'FactorZ'), s);
  // map = (E, N, H) + R(θ) · S · model, θ from the map's east axis to the model's x axis
  const m = [ax * s, ay * s, 0, 0, -ay * sy, ax * sy, 0, 0, 0, 0, sz, 0, e, n, h, 1];
  return { kind: 'map-conversion', crs, eastings: e, northings: n, height: h, xAxis: [ax, ay], rotation: Math.atan2(ay, ax), scale: s, matrix: m };
};
ifc/geo-math.js BimCraft 0.8.1 64-bit 6 calculations Reviewed

Tests tools/ifc-test/nurbs.mjs

Matrix product

64-bit

The product of two 4 × 4 matrices.

Each entry is a sum of four products in 64-bit, in a fixed order. No special case is made for the bottom row.

  • ifc/geo-math.js
  • lines 10 to 20
  • BimCraft 0.8.1
export function mul(a, b) {
  const o = new Array(16);
  for (let c = 0; c < 4; c++) {
    const b0 = b[c * 4], b1 = b[c * 4 + 1], b2 = b[c * 4 + 2], b3 = b[c * 4 + 3];
    o[c * 4] = a[0] * b0 + a[4] * b1 + a[8] * b2 + a[12] * b3;
    o[c * 4 + 1] = a[1] * b0 + a[5] * b1 + a[9] * b2 + a[13] * b3;
    o[c * 4 + 2] = a[2] * b0 + a[6] * b1 + a[10] * b2 + a[14] * b3;
    o[c * 4 + 3] = a[3] * b0 + a[7] * b1 + a[11] * b2 + a[15] * b3;
  }
  return o;
}

Point through a matrix

64-bit

Transforms a point by a matrix.

p' = M · (x, y, z, 1)

Three sums of three products plus the translation, in 64-bit. When the translation is much larger than the point, the low digits of the point are lost in the sum.

  • ifc/geo-math.js
  • lines 22 to 24
  • BimCraft 0.8.1
export function apply(m, x, y, z) {
  return [m[0] * x + m[4] * y + m[8] * z + m[12], m[1] * x + m[5] * y + m[9] * z + m[13], m[2] * x + m[6] * y + m[10] * z + m[14]];
}

Matrix inverse

64-bit

The inverse of a 4 × 4 matrix, or null when it has none.

M⁻¹ = adjugate ÷ determinant

The sixteen cofactors are written out as sums of triple products and divided by the determinant. Only a determinant of exactly 0 is rejected, so a nearly singular matrix returns an inverse with few good digits.

  • ifc/geo-math.js
  • lines 35 to 57
  • BimCraft 0.8.1
export function invert(m) {
  const a = m, o = new Array(16);
  o[0] = a[5] * a[10] * a[15] - a[5] * a[11] * a[14] - a[9] * a[6] * a[15] + a[9] * a[7] * a[14] + a[13] * a[6] * a[11] - a[13] * a[7] * a[10];
  o[4] = -a[4] * a[10] * a[15] + a[4] * a[11] * a[14] + a[8] * a[6] * a[15] - a[8] * a[7] * a[14] - a[12] * a[6] * a[11] + a[12] * a[7] * a[10];
  o[8] = a[4] * a[9] * a[15] - a[4] * a[11] * a[13] - a[8] * a[5] * a[15] + a[8] * a[7] * a[13] + a[12] * a[5] * a[11] - a[12] * a[7] * a[9];
  o[12] = -a[4] * a[9] * a[14] + a[4] * a[10] * a[13] + a[8] * a[5] * a[14] - a[8] * a[6] * a[13] - a[12] * a[5] * a[10] + a[12] * a[6] * a[9];
  o[1] = -a[1] * a[10] * a[15] + a[1] * a[11] * a[14] + a[9] * a[2] * a[15] - a[9] * a[3] * a[14] - a[13] * a[2] * a[11] + a[13] * a[3] * a[10];
  o[5] = a[0] * a[10] * a[15] - a[0] * a[11] * a[14] - a[8] * a[2] * a[15] + a[8] * a[3] * a[14] + a[12] * a[2] * a[11] - a[12] * a[3] * a[10];
  o[9] = -a[0] * a[9] * a[15] + a[0] * a[11] * a[13] + a[8] * a[1] * a[15] - a[8] * a[3] * a[13] - a[12] * a[1] * a[11] + a[12] * a[3] * a[9];
  o[13] = a[0] * a[9] * a[14] - a[0] * a[10] * a[13] - a[8] * a[1] * a[14] + a[8] * a[2] * a[13] + a[12] * a[1] * a[10] - a[12] * a[2] * a[9];
  o[2] = a[1] * a[6] * a[15] - a[1] * a[7] * a[14] - a[5] * a[2] * a[15] + a[5] * a[3] * a[14] + a[13] * a[2] * a[7] - a[13] * a[3] * a[6];
  o[6] = -a[0] * a[6] * a[15] + a[0] * a[7] * a[14] + a[4] * a[2] * a[15] - a[4] * a[3] * a[14] - a[12] * a[2] * a[7] + a[12] * a[3] * a[6];
  o[10] = a[0] * a[5] * a[15] - a[0] * a[7] * a[13] - a[4] * a[1] * a[15] + a[4] * a[3] * a[13] + a[12] * a[1] * a[7] - a[12] * a[3] * a[5];
  o[14] = -a[0] * a[5] * a[14] + a[0] * a[6] * a[13] + a[4] * a[1] * a[14] - a[4] * a[2] * a[13] - a[12] * a[1] * a[6] + a[12] * a[2] * a[5];
  o[3] = -a[1] * a[6] * a[11] + a[1] * a[7] * a[10] + a[5] * a[2] * a[11] - a[5] * a[3] * a[10] - a[9] * a[2] * a[7] + a[9] * a[3] * a[6];
  o[7] = a[0] * a[6] * a[11] - a[0] * a[7] * a[10] - a[4] * a[2] * a[11] + a[4] * a[3] * a[10] + a[8] * a[2] * a[7] - a[8] * a[3] * a[6];
  o[11] = -a[0] * a[5] * a[11] + a[0] * a[7] * a[9] + a[4] * a[1] * a[11] - a[4] * a[3] * a[9] - a[8] * a[1] * a[7] + a[8] * a[3] * a[5];
  o[15] = a[0] * a[5] * a[10] - a[0] * a[6] * a[9] - a[4] * a[1] * a[10] + a[4] * a[2] * a[9] + a[8] * a[1] * a[6] - a[8] * a[2] * a[5];
  const det = a[0] * o[0] + a[1] * o[4] + a[2] * o[8] + a[3] * o[12];
  if (!det) return null;
  for (let i = 0; i < 16; i++) o[i] /= det;
  return o;
}

Rotation about an axis

64-bit

The matrix that turns by an angle about an axis through the origin.

R = c I + s [a]× + (1 − c) a aᵀ

The axis is normalized first; a zero axis becomes (0, 0, 0) and gives a matrix that is not a rotation. For very small angles 1 − cos a loses digits, which only affects terms that are themselves very small.

  • ifc/geo-math.js
  • lines 77 to 81
  • BimCraft 0.8.1
/** Rotation about a unit axis through the origin by angle a (radians). */
export function rotation(axis, a) {
  const [x, y, z] = norm(axis), c = Math.cos(a), s = Math.sin(a), t = 1 - c;
  return [t * x * x + c, t * x * y + s * z, t * x * z - s * y, 0, t * x * y - s * z, t * y * y + c, t * y * z + s * x, 0, t * x * z + s * y, t * y * z - s * x, t * z * z + c, 0, 0, 0, 0, 1];
}

Polygon normal

64-bit

The normal of a 3D polygon, with a length of twice its area.

n = Σ ((yₐ − y_b)(zₐ + z_b), (zₐ − z_b)(xₐ + x_b), (xₐ − x_b)(yₐ + y_b))

Newell's method, summed over the edges in order. Each term multiplies a coordinate difference by a coordinate sum, so for a polygon far from the origin the terms are large and nearly cancel.

  • ifc/geo-math.js
  • lines 83 to 93
  • BimCraft 0.8.1
/** Newell normal of a 3D polygon [[x,y,z]…] (not normalised; length = 2 × area). */
export function newell(pts) {
  let nx = 0, ny = 0, nz = 0;
  for (let i = 0, n = pts.length; i < n; i++) {
    const a = pts[i], b = pts[(i + 1) % n];
    nx += (a[1] - b[1]) * (a[2] + b[2]);
    ny += (a[2] - b[2]) * (a[0] + b[0]);
    nz += (a[0] - b[0]) * (a[1] + b[1]);
  }
  return [nx, ny, nz];
}

Signed area of a 2D polygon

64-bit

The area of a profile outline, positive when counter-clockwise; used to orient outlines and holes.

A = Σ (xₐ y_b − x_b yₐ) ÷ 2

The shoelace sum in absolute coordinates, halved once at the end. Profiles are defined near their own origin, so the products stay small.

  • ifc/geo-math.js
  • lines 95 to 100
  • BimCraft 0.8.1
/** Signed area of a 2D polygon [[x,y]…] (counter-clockwise positive). */
export function area2(pts) {
  let s = 0;
  for (let i = 0, n = pts.length; i < n; i++) { const a = pts[i], b = pts[(i + 1) % n]; s += a[0] * b[1] - b[0] * a[1]; }
  return s / 2;
}

In detail

Method
Frames from an origin and unit axes (the IFC first-projection-axis rule); matrix product along the placement chain; grid placement as the intersection of two straight axes.
Number types
64-bit numbers; matrices are plain arrays of 16 numbers, column-major.
Tolerances
Two grid axes are parallel when the cross product of their unit directions is under 1e-12. The default X axis switches to (0, 1, 0) when Z is within 1e-6 of the X axis. A matrix is singular only when its determinant is exactly 0.
Limits
A conversion factor stated in the file is used as written, so a degree factor given with few digits limits every angle converted with it. A unit known only by a name containing INCH or FOOT gets the length factor whatever its unit type. A matrix inverse is the adjugate divided by the determinant, with no pivoting.

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.

Every policy that takes this approach