Lengths, bearings, grades and area
64-bitEvery number the Measure tool shows for a picked line or shape.
L = hypot(Δx, Δy); bearing = atan2(Δy, Δx); grade % = Δz ÷ L × 100; L₃ = hypot(L, Δz); area = |Σ (x_i·y_j − x_j·y_i)| ÷ 2
Lengths and bearings use differences of the picked points, so they keep their digits. The area sum multiplies absolute coordinates: with coordinates near 2,000,000 each product is about 4e12, where 64-bit numbers are about 0.001 apart, so the area is resolved to about 0.001 square units per corner instead of 15 digits. Totals are plain sums.
export function measure(pts, { closed = false, zAt = null } = {}) {
const P = (Array.isArray(pts) ? pts : []).filter((p) => p && fin(p.x) && fin(p.y));
const z = (p) => { if (fin(p.z)) return p.z; try { const v = zAt ? zAt(p.x, p.y) : null; return fin(v) ? v : null; } catch { return null; } };
const Z = P.map(z);
const shut = closed && P.length >= 3;
const segs = [];
const seg = (i, j) => {
const a = P[i], b = P[j], L = Math.hypot(b.x - a.x, b.y - a.y);
const dz = Z[i] != null && Z[j] != null ? Z[j] - Z[i] : null;
return { L, bearing: Math.atan2(b.y - a.y, b.x - a.x), dz, grade_pct: dz != null && L > 1e-12 ? (dz / L) * 100 : null, L3d: dz != null ? Math.hypot(L, dz) : null };
};
for (let i = 1; i < P.length; i++) segs.push(seg(i - 1, i));
if (shut) segs.push(seg(P.length - 1, 0));
const total = segs.reduce((s, x) => s + x.L, 0);
const total3d = segs.length && segs.every((x) => x.L3d != null) ? segs.reduce((s, x) => s + x.L3d, 0) : null;
let area = null;
if (shut) {
let a2 = 0;
for (let i = 0; i < P.length; i++) { const p = P[i], q = P[(i + 1) % P.length]; a2 += p.x * q.y - q.x * p.y; }
area = Math.abs(a2) / 2;
}
return { n: P.length, closed: shut, segs, total, total3d, area, last: segs.length ? segs[shut ? segs.length - 2 : segs.length - 1] || null : null };
}