Resources Numerical accuracy Calculation policy

Coordinate readouts and survey point

Says where a point is: its distance from the project origin, the level it is on, its height above that level and its map grid coordinates.

How the precision is kept

Millimeters and meters in 64-bit. Coordinates pass through 4 × 4 matrices from ifc-util.js, and the survey point is read from the site's placement. A map value is divided by the map unit's size as the file gives it and rounded only when it is written as text.

Calculations under this policy

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

coords.js BimCraft 0.8.1 64-bit 8 calculations Reviewed

Tests tools/bimcraft-test/draw06-test.mjs, tools/bimcraft-test/gh3ui-test.mjs

Project origin and survey point

64-bit

Where the site's survey point is in the file's coordinates, with its elevation and map grid position.

survey point = site placement origin × file unit in mm; elevation in m = RefElevation × unit ÷ 1000

The survey point is the translation part of the site's world matrix times the file's unit in millimeters. Origin and survey point count as one marker when the survey point is under 1 mm from the origin (Math.hypot). Latitude and longitude are kept only within ±90° and ±180°.

  • coords.js
  • lines 23 to 41
  • BimCraft 0.8.1
export function readMarkers(api, mid) {
  const unitMm = lengthUnitMm(api, mid);
  let georef = null;
  try { georef = readGeoref(api, mid); } catch (e) { georef = null; }
  let survey = null;
  const siteId = ids(api, mid, 'IFCSITE')[0];
  if (siteId !== undefined) {
    const s = api.GetLine(mid, siteId, false);
    let at = [0, 0, 0];
    try { if (s.ObjectPlacement && api.GetWorldTransformMatrix) { const M = api.GetWorldTransformMatrix(mid, s.ObjectPlacement.value); at = [M[12] * unitMm, M[13] * unitMm, M[14] * unitMm]; } } catch (e) { /* at the file's origin */ }
    const lat = compound(s.RefLatitude), lon = compound(s.RefLongitude);
    const geo = lat !== null && lon !== null && Math.abs(lat) <= 90 && Math.abs(lon) <= 180;
    const elev = s.RefElevation !== null && s.RefElevation !== undefined ? (Number(val(s.RefElevation)) || 0) * unitMm / 1000 : null;
    const map = georef?.kind === 'map' ? fileToMap(georef, at.map((v) => v / unitMm)) : null;
    survey = { at, lat: geo ? lat : null, lon: geo ? lon : null, elev, map };
  }
  const same = !survey || Math.hypot(...survey.at) < 1;
  return { unitMm, origin: [0, 0, 0], survey, georef, same };
}

Level a height is on

64-bit

Picks the level whose floor is at or just below a height.

the highest level with elevation ≤ z + 1 mm

A 1 mm absolute allowance puts a point on a level's floor on that level. Below every level the lowest one is returned.

  • coords.js
  • lines 43 to 48
  • BimCraft 0.8.1
export function levelFor(levels, zMm) {
  if (!levels?.length || !Number.isFinite(zMm)) return null;
  let best = null;
  for (const l of levels) if (l.elevation <= zMm + 1 && (!best || l.elevation > best.elevation)) best = l;
  return best || levels.reduce((a, b) => (b.elevation < a.elevation ? b : a));
}

Foot and US survey foot

Exact

The two sizes of foot a map's unit is compared with.

ft = 0.3048 m; US ft = 1200 ÷ 3937 m

0.3048 is the foot's exact definition and 1200 ÷ 3937 the US survey foot's exact fraction, evaluated once in 64-bit. They are used to recognize the map's unit, not to convert.

  • coords.js
  • line 50
  • BimCraft 0.8.1
const FT = 0.3048, USFT = 1200 / 3937;

Map grid coordinates as text

64-bit

A point's eastings and northings in the map's own unit.

value = meters ÷ map unit in meters, written with 3 decimals

The metric coordinate is divided by the map unit's size as the georeference gives it, in 64-bit, and written with exactly 3 decimals by toLocaleString. That rounding is of the text only. In the person's display unit the value goes to model-units.js instead.

  • coords.js
  • lines 51 to 61
  • BimCraft 0.8.1
export function mapText(georef, eM, nM, units = null) {
  if (units) {   // GH.3-A: the person's display unit (eM, nM in metres, as fileToMap gives them); the map's own text is kept in `own`
    const own = mapText(georef, eM, nM), M = 1000;   // eM, nM are metres (fileToMap)
    return { e: coordNumber(eM * M, units), n: coordNumber(nM * M, units), unit: unitMark(units === 'ftin' ? 'ftdec' : units), own, note: crsUnitNote(units, own.unit) };   // note: the CRS's own unit id when it differs ('ft', 'us-ft', 'm')
  }
  const u = georef?.mapUnitM || 1;
  const unit = Math.abs(u - FT) < 1e-9 ? 'ft' : Math.abs(u - USFT) < 1e-9 ? 'US ft' : 'm';   // the two feet differ by 2 ppm   // i18n-ignore: unit symbols
  const k = unit === 'm' ? 1 : u;
  const f = (v) => (v / k).toLocaleString('en-US', { minimumFractionDigits: 3, maximumFractionDigits: 3 }).replace(/,/g, ' ');
  return { e: f(eM), n: f(nM), unit };
}

Height above the level

64-bit

The lines of a readout: the level and the height above its floor, X and Y from the project origin, Z, and the map position.

height above the level = z − level elevation

One 64-bit subtraction in millimeters. Every value is handed to the unit formatter that is passed in; nothing is rounded here.

  • coords.js
  • lines 63 to 73
  • BimCraft 0.8.1
/**
 * The lines of a readout. p = { x, y (mm from the project origin), z (mm, or null in the plan), level ({ name, elevation }),
 * map ({ e, n, unit } | null) }. The 3D view gives z; the line under the level then says how high above its floor.
 */
export function readout(p, fmt, t) {
  const lines = [];
  if (p.level) lines.push(Number.isFinite(p.z) ? t('coords.level.at', { level: p.level.name, h: fmt(p.z - p.level.elevation) }) : p.level.name);
  if (Number.isFinite(p.x)) lines.push(t('coords.xy', { x: fmt(p.x), y: fmt(p.y) }) + (Number.isFinite(p.z) ? '  ' + t('coords.z', { z: fmt(p.z) }) : ''));
  if (p.map) lines.push(t('coords.map', { e: p.map.e, n: p.map.n, unit: p.map.unit }));
  return lines;
}

Plan point to file coordinates

64-bit

A point in a storey's plan, in the file's coordinates.

q = M (x, y, 0)

One 4 × 4 matrix applied in 64-bit (ifc-util.js apply4).

  • coords.js
  • line 75
  • BimCraft 0.8.1
export function planToFile(M, p) { return apply4(M, p[0], p[1], 0); }

File coordinates to plan point

64-bit

A point in the file's coordinates, in a storey's plan.

p = M⁻¹ q

The storey matrix is inverted on each call (ifc-util.js inv4) and applied in 64-bit. The height is dropped.

  • coords.js
  • line 76
  • BimCraft 0.8.1
export function fileToPlan(M, q) { const r = apply4(inv4(M), q[0], q[1], q[2] || 0); return [r[0], r[1]]; }

File coordinates to the 3D view

64-bit

Where a point of the file sits in the 3D view, to draw the origin and survey markers.

(x, y, z) × unit → (x, z, −y) → C → placement

Scales by the file unit in meters, changes to Y up by a swap and a sign (exact), then applies two 4 × 4 matrices in 64-bit.

  • coords.js
  • lines 78 to 82
  • BimCraft 0.8.1
export function fileToViewer(p, place, C, fileUnitM) {
  const m = [p[0] * fileUnitM, p[2] * fileUnitM, -p[1] * fileUnitM];   // Z up → Y up, metres, where the file has it
  const c = apply4(C, m[0], m[1], m[2]);
  return apply4(place, c[0], c[1], c[2]);
}

In detail

Method
Matrix products and inverses for plan, file and viewer coordinates; the level is the highest whose floor is at or below the point.
Number types
64-bit numbers; matrices as arrays of 16.
Tolerances
A point up to 1 mm below a level's floor is on that level. Origin and survey point are one marker when under 1 mm apart. The map unit is taken as the foot or the US survey foot when within 1e-9 m of 0.3048 or 1200 ÷ 3937.
Limits
Map text in the map's own unit has 3 decimals.

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