Resources Numerical accuracy Calculation policy

3D check view exported as glTF

Writes the 3D check view to a binary glTF file in meters.

How the precision is kept

The conversion from feet to meters is done in 64-bit with the exact factor 0.3048, but the vertex positions are then written as 32-bit floats, which is how glTF 2.0 stores them here (componentType 5126). Positions are in world coordinates with no local origin, so a 32-bit value has about 7 digits over the whole sheet. Each object's measured quantities travel in the file's JSON as 64-bit numbers and are not affected.

Open item. Write each object's positions relative to a local origin carried in its node's translation (a 64-bit number in the JSON), so the 32-bit vertex values stay small.

Calculations under this policy

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

glb.js TakeoffCraft 0.8.1 32-bit in part 4 calculations Reviewed

Tests bin/test-takeoffcraft.mjs

Feet to meters, Z up to Y up

64-bit

Converts one vertex from the view's feet to glTF's meters and axes.

(x, y, z) ft → (0.3048·x, 0.3048·z, −0.3048·y) m

Three multiplications by 0.3048, the exact definition of the foot, in 64-bit. The swap of axes changes no digits.

  • glb.js
  • line 26
  • TakeoffCraft 0.8.1
const toGl = (a, i) => [a[i] * M, a[i + 2] * M, -a[i + 1] * M];

Vertex positions and normals written

32-bit in part

Fills the position and normal buffers of an object.

n = (b − a) × (c − a) ÷ |…|

The converted meters are stored into Float32Array buffers with pos.set, which rounds each coordinate to 32-bit. The normal is computed in 64-bit from the unrounded vertices and then stored in 32-bit; a zero-area triangle gets the normal (0, 1, 0).

  • glb.js
  • lines 64 to 77
  • TakeoffCraft 0.8.1
const pos = new Float32Array(n * 3), nor = new Float32Array(n * 3);
for (let v = 0; v < n; v += 3) {
  const a = toGl(o.tris, v * 3), b = toGl(o.tris, v * 3 + 3), c = toGl(o.tris, v * 3 + 6);
  const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2], vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
  let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
  const l = Math.hypot(nx, ny, nz);
  if (l > 0) { nx /= l; ny /= l; nz /= l; } else { nx = 0; ny = 1; nz = 0; }       // a degenerate sliver: any unit normal
  for (let k = 0; k < 3; k++) {
    const p = [a, b, c][k];
    pos.set(p, (v + k) * 3);
    nor.set([nx, ny, nz], (v + k) * 3);
  }
}
prims.push({ attributes: { POSITION: addAccessor(pos, 'VEC3', true), NORMAL: addAccessor(nor, 'VEC3', false) }, material: material(o.color, o.kind === 'slab' || o.kind === 'plate'), mode: 4 });

Bounds of each position buffer

32-bit in part

The minimum and maximum of a position buffer, written beside it.

min and max are taken over the Float32Array after the positions were stored, so they agree with the stored 32-bit values and not with the 64-bit ones.

  • glb.js
  • lines 46 to 57
  • TakeoffCraft 0.8.1
const addAccessor = (arr, type, withBounds) => {
  bufferViews.push({ buffer: 0, byteOffset: byteLength, byteLength: arr.byteLength, target: 34962 });
  chunks.push(arr);
  byteLength += arr.byteLength;                            // Float32: always a multiple of 4
  const acc = { bufferView: bufferViews.length - 1, componentType: 5126, count: arr.length / 3, type };
  if (withBounds) {
    const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
    for (let i = 0; i < arr.length; i += 3) for (let k = 0; k < 3; k++) { min[k] = Math.min(min[k], arr[i + k]); max[k] = Math.max(max[k], arr[i + k]); }
    acc.min = min; acc.max = max;
  }
  accessors.push(acc);
  return accessors.length - 1;

Quantities carried with each object

64-bit

Attaches each object's mark, item, kind and measured values to its node.

The measure object from the 3D check view is put in the node's extras, which are part of the file's JSON chunk, so its values are written as 64-bit numbers in text and do not pass through a 32-bit buffer. They stay in feet, square feet and cubic feet.

  • glb.js
  • line 85
  • TakeoffCraft 0.8.1
nodes.push({ name: (o.name || '') + ' (' + o.kind + ')', mesh: meshes.length - 1, extras: { mark: o.id, item: o.item, kind: o.kind, measure: o.measure } });

In detail

Method
(x, y, z) in feet, Z up, becomes (x, z, −y) × 0.3048 in meters, Y up. One node and mesh per object, flat normals from the cross product of two edges.
Number types
64-bit numbers for the conversion; Float32Array for positions and normals; the accessor bounds are taken from the stored 32-bit values; quantities in the JSON chunk are 64-bit.
Limits
Between 256 and 512 meters from the scene's origin a 32-bit float moves in steps of 0.03 mm (2⁻¹⁵ m); the steps double with each doubling of the distance.

Approach: 32-bit step, flagged

One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.

Every policy that takes this approach