Elevation grids into survey points and a grid surface
Places the cells of an elevation raster in the project's coordinates, converts heights between meters and feet, and makes survey points or a triangulated surface from the grid.
How the precision is kept
A cell center is the raster's geotransform applied to column + 0.5 and row + 0.5 in 64-bit, then passed through the coordinate conversion. Heights are the file's values times one unit factor. The surface is two flat triangles per cell, so it equals the grid at the nodes and is linear between them, as the file's header says.
Calculations under this policy
7 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.
Two products and two sums per coordinate from the six geotransform numbers; adding 0.5 to a whole column or row is exact.
terrain.js
lines 114 to 117
CadCraft 0.8.2
export function nodeXY(src, col, row) {
const g = src.gt, c = col + 0.5, r = row + 0.5;
return [g[0] + c * g[1] + r * g[2], g[3] + c * g[4] + r * g[5]];
}
Cells inside a box
64-bit
The range of columns and rows whose centers can fall inside a picked box.
The inverse divides by the geotransform's determinant, refused only when it is exactly zero. The four box corners are mapped and the range rounded outward with 1e-9 of a cell of slack, then held inside the raster.
terrain.js
lines 119 to 131
CadCraft 0.8.2
/** Cells (half-open [c0, c1) × [r0, r1)) whose centers may fall in the box (source coordinates). */
export function gridWindow(src, box) {
const g = src.gt, det = g[1] * g[5] - g[2] * g[4];
if (!det || !box) return null;
const inv = (x, y) => { const dx = x - g[0], dy = y - g[3]; return [(g[5] * dx - g[2] * dy) / det - 0.5, (-g[4] * dx + g[1] * dy) / det - 0.5]; };
let c0 = Infinity, r0 = Infinity, c1 = -Infinity, r1 = -Infinity;
for (const [x, y] of [[box.minX, box.minY], [box.maxX, box.minY], [box.maxX, box.maxY], [box.minX, box.maxY]]) {
const [c, r] = inv(x, y);
c0 = Math.min(c0, c); c1 = Math.max(c1, c); r0 = Math.min(r0, r); r1 = Math.max(r1, r);
}
const w = { c0: Math.max(0, Math.ceil(c0 - 1e-9)), r0: Math.max(0, Math.ceil(r0 - 1e-9)), c1: Math.min(src.width, Math.floor(c1 + 1e-9) + 1), r1: Math.min(src.height, Math.floor(r1 + 1e-9) + 1) };
return w.c1 > w.c0 && w.r1 > w.r0 ? w : null;
}
A box through a coordinate conversion
64-bit
The box round a rectangle after its outline is converted to another coordinate system.
17 points along each side, converted, then minimum and maximum
The outline is sampled, not solved: where the converted side bulges between two samples the box is smaller than the true one by that bulge. Points that convert to non-finite values are skipped.
terrain.js
lines 104 to 112
CadCraft 0.8.2
export function boxThrough(b, fn, n = 16) {
const o = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
const add = (x, y) => { const [u, v] = fn(x, y); if (!Number.isFinite(u) || !Number.isFinite(v)) return; o.minX = Math.min(o.minX, u); o.maxX = Math.max(o.maxX, u); o.minY = Math.min(o.minY, v); o.maxY = Math.max(o.maxY, v); };
for (let i = 0; i <= n; i++) {
const t = i / n, x = b.minX + (b.maxX - b.minX) * t, y = b.minY + (b.maxY - b.minY) * t;
add(x, b.minY); add(x, b.maxY); add(b.minX, y); add(b.maxX, y);
}
return Number.isFinite(o.minX) ? o : null;
}
Survey points from a grid
64-bit
One survey point per grid node that has a height.
(x, y) = conversion(cell center); z = file height × unit factor
The height is multiplied once by the unit factor and the position is the converted cell center; nothing is rounded. Nodes without data are skipped.
terrain.js
lines 139 to 151
CadCraft 0.8.2
export function gridPoints(grid, src, { fn, zf = 1, desc = 'DEM', start = 1 }) {
const out = [];
let n = start;
for (let j = 0; j < grid.ny; j++) for (let i = 0; i < grid.nx; i++) {
const z = grid.z[j * grid.nx + i];
if (Number.isNaN(z)) continue;
const [sx, sy] = nodeXY(src, grid.c0 + i * grid.step, grid.r0 + j * grid.step);
const [x, y] = fn(sx, sy);
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
out.push({ n: String(n++), x, y, z: z * zf, d: desc });
}
return out;
}
Grid surface
64-bit
A triangulated surface from the grid: two triangles per cell.
cell corners a (SW), b (SE), c (NE), d (NW): triangles a–b–c and a–c–d
Node positions and heights are computed as for the survey points. Every cell is split along the same diagonal whatever the ground does, and the surface is flat inside each triangle. A triangle with a corner that has no data is left out and counted.
terrain.js
lines 153 to 175
CadCraft 0.8.2
export function gridTin(grid, src, { fn, zf = 1 }) {
const { nx, ny, z } = grid, idx = new Int32Array(nx * ny).fill(-1), pts = [];
for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
const v = z[j * nx + i];
if (Number.isNaN(v)) continue;
const [sx, sy] = nodeXY(src, grid.c0 + i * grid.step, grid.r0 + j * grid.step);
const [x, y] = fn(sx, sy);
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
idx[j * nx + i] = pts.length;
pts.push({ x, y, z: v * zf });
}
const faces = [];
let cells = 0, skipped = 0;
// row j runs north → south (row order of the file); the cell (i, j) has corners a = (i, j+1) SW, b = (i+1, j+1) SE,
// c = (i+1, j) NE, d = (i, j) NW; triangles a-b-c and a-c-d share the SW–NE diagonal
for (let j = 0; j + 1 < ny; j++) for (let i = 0; i + 1 < nx; i++) {
const a = idx[(j + 1) * nx + i], b = idx[(j + 1) * nx + i + 1], c = idx[j * nx + i + 1], d = idx[j * nx + i];
cells++;
if (a >= 0 && b >= 0 && c >= 0) faces.push([a, b, c]); else skipped++;
if (a >= 0 && c >= 0 && d >= 0) faces.push([a, c, d]); else skipped++;
}
return { pts, faces, cells, skipped };
}
1-2-5 step
64-bit
Rounds a step up to 1, 2 or 5 times a power of ten.
p = 10^⌊log₁₀ x⌋; m = x ÷ p; 1, 2, 5 or 10 × p
Each comparison allows 1e-9 above the step, so a value a rounding error above 2 still gives 2. The result is the step times the power of ten p as computed in 64-bit.
terrain.js
lines 177 to 182
CadCraft 0.8.2
/** 1-2-5 rounding (up). */
export function niceStep(x) {
if (!(x > 0) || !Number.isFinite(x)) return 1;
const p = 10 ** Math.floor(Math.log10(x)), m = x / p;
return (m <= 1 + 1e-9 ? 1 : m <= 2 + 1e-9 ? 2 : m <= 5 + 1e-9 ? 5 : 10) * p;
}
In detail
Method
Affine geotransform and its inverse; two triangles per cell on the south-west to north-east diagonal; a box taken through a transform by sampling its outline.
Number types
64-bit numbers; the header describes grid heights as a Float64Array.
Tolerances
Window edges allow 1e-9 of a cell before rounding to whole cells. 1-2-5 rounding allows 1e-9 above each step. Box outline sampled at 17 points per side.
Limits
The surface is planar inside each triangle. A box through a curved transform is the box of sampled outline points, so it can be slightly small. A triangle with a missing corner is left out.
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.