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.jsTakeoffCraft 0.8.1 32-bit in part4 calculationsReviewed
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.
(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.