Reads lengths and stations as typed, and writes stations, areas, volumes and bearings as text.
How the precision is kept
The length factors are exact: 0.0254 m per inch, 0.3048 m per foot, and 1200 ÷ 3937 m per US survey foot written as that fraction. Typed values are read with Number and combined in 64-bit without rounding. Text output is rounded to a stated number of decimals. One mix exists by stated choice: in a feet project (US survey feet) a length typed in mm, cm or m is converted with the international foot, while other unit marks go through the shared unit table to US survey feet.
Open item. Convert mm, cm and m typed into a feet project with 1200 ÷ 3937 m per foot, the same foot the project and the unit table use.
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.
The factor for each linear unit name a LandXML file can carry.
US survey foot = 1200 ÷ 3937 m; foot = 0.3048 m
The US survey foot is written as the fraction 1200 ÷ 3937, its definition, and evaluated once in 64-bit; 0.3048 is the exact international foot. No rounded factor is used.
units.js
lines 39 to 40
CivilCraft 0.8.1
/** Metres per unit for linear units found in files (LandXML linearUnit names). */
export const METRES_PER = { USSurveyFoot: 1200 / 3937, foot: 0.3048, ft: 0.3048, meter: 1, m: 1, metre: 1, millimeter: 0.001, centimeter: 0.01, kilometer: 1000 };
One typed length
64-bit
Reads one length as typed (plain number, feet and inches, fraction, mm, cm or m) into project units.
The factors 12 and 0.0254 are exact and the arithmetic is 64-bit with no rounding. In a feet project a metric length is turned into international feet (÷ 0.0254 ÷ 12) although the project's feet are US survey feet, so 1000 m and 1 km typed into a feet project differ by 2 parts per million; the file header states this choice. A fraction with a zero denominator returns no value.
units.js
lines 69 to 88
CivilCraft 0.8.1
/** One length as CivilCraft reads it → its value in project units (null when it is not one). */
function oneLength(x, unit) {
let s = String(x).trim().toLowerCase().replace(/\s+/g, ' ');
if (isNum(s)) return Number(s);
const toUnit = (metres) => (unit === 'm' ? metres : metres / M_PER_IN / 12); // feet project: inches to feet as 12 to 1
const m = s.match(/^(\d+(?:\.\d*)?|\.\d+) ?(mm|cm|m)$/);
if (m) return toUnit(Number(m[1]) * (m[2] === 'mm' ? 0.001 : m[2] === 'cm' ? 0.01 : 1));
// a number with a fraction: 4 7/16, 4-7/16, 7/16 (in the unit it stands in)
const frac = (q) => { const f = q.trim().match(/^(?:(\d+)[ -])?(\d+)\/(\d+)$/); if (f) return f[3] === '0' ? null : (Number(f[1] || 0) + Number(f[2]) / Number(f[3])); return isNum(q.trim()) ? Number(q) : null; };
const bare = frac(s);
if (bare !== null) return bare;
// feet and inches: 12'6", 12' 6 1/2", 12'-6", 6", 3 ft 6 in, 3ft, 6 in
s = s.replace(/\b(feet|foot|ft)\b\.?/g, "'").replace(/\b(inches|inch|in)\b\.?/g, '"').replace(/''/g, '"');
const fi = s.match(/^(?:([\d./ -]+?) ?')?(?: ?-? ?([\d./ -]+?) ?")?$/);
if (!fi || (!fi[1] && !fi[2])) return tableLength(x, unit);
const ft = fi[1] ? frac(fi[1]) : 0, inch = fi[2] ? frac(fi[2]) : 0;
if (ft === null || inch === null) return null;
const feet = ft + inch / 12;
return unit === 'm' ? feet * 12 * M_PER_IN : feet;
}
Station text
64-bit
Writes a station as text, such as 12+34.56 in feet or 1+234.567 in meters.
r = round(|sta|, decimals); whole = floor(r ÷ station length + 1e-9); rest = r − whole × station length
The value is rounded to the shown decimals before it is split, so a station just under a whole station does not print as 9+100.00. The 1e-9 guards the floor against a quotient that lands just below a whole number. Text only; the stored station is not changed.
units.js
lines 42 to 54
CivilCraft 0.8.1
/** "12+34.56" (ft) or "1+234.567" (m). Negative stations keep their sign in front: "-0+50.00". */
export function formatStation(sta, unit = 'ft', dec) {
if (!Number.isFinite(sta)) return '';
const u = unitOf(unit), d = dec ?? u.staDec;
const neg = sta < 0;
const r = Number(Math.abs(sta).toFixed(d));
const big = Math.floor(r / u.staLen + 1e-9);
let rest = r - big * u.staLen;
if (rest < 0) rest = 0;
const w = String(u.staLen).length - 1; // digits before the point: 2 for ft, 3 for m
const restStr = rest.toFixed(d).padStart(w + (d ? d + 1 : 0), '0');
return (neg ? '-' : '') + big + '+' + restStr;
}
Station as typed
64-bit
Reads a station typed as 12+34.56 or as a plain number.
value = whole × station length + rest
Both parts are read with Number and combined with one multiplication by a whole number and one addition, so nothing is rounded.
units.js
lines 56 to 66
CivilCraft 0.8.1
/** Parses "12+34.56", "1234.56", "-0+50" → number (in the project unit), or null. */
export function parseStation(s, unit = 'ft') {
const t = String(s ?? '').trim().replace(/\s+/g, '');
if (!t) return null;
const m = t.match(/^([+-]?)(\d+)\+(\d+(?:\.\d*)?)$/);
if (m) {
const v = Number(m[2]) * unitOf(unit).staLen + Number(m[3]);
return m[1] === '-' ? -v : v;
}
return isNum(t) ? Number(t) : null;
}
Volume text
64-bit
Writes a volume in cubic yards or cubic meters, or in the volume unit chosen in Settings.
feet: V ÷ 27 cu yd; chosen unit: V × (m per unit)³ through the shared unit table
27 cubic feet per cubic yard is exact. For a chosen unit the value is first put in cubic meters with (1200 ÷ 3937)³ for feet projects, then converted by the shared table. The text is rounded to the stated decimals with thousands separators.
units.js
lines 123 to 131
CivilCraft 0.8.1
/**
* A volume in the unit's report unit (cu yd for feet, cu m for metres), or in the volume unit starred in Settings ›
* General › Units (0.7 UNITS: acre-feet, litres …); the same for areas (acres, hectares …).
*/
export function volumeText(v, unit = 'ft', d = 1) {
const s = starred('volume', unit, { ft: 'yd3', m: 'm3' });
if (s) return grouped(fromBase(v * (M_PER_UNIT[unit] || M_PER_UNIT.ft) ** 3, s.id), s.dec) + ' ' + s.mark;
const u = unitOf(unit); return grouped(v / u.volPer, d) + ' ' + u.vol;
}
Area text
64-bit
Writes an area in square feet or square meters, or in the area unit chosen in Settings.
chosen unit: A × (m per unit)² through the shared unit table
For a chosen unit the value is first put in square meters with (1200 ÷ 3937)² for feet projects. The text is rounded to the stated decimals, 0 by default.
units.js
lines 132 to 136
CivilCraft 0.8.1
export function areaText(a, unit = 'ft', d = 0) {
const s = starred('area', unit, { ft: 'ft2', m: 'm2' });
if (s) return grouped(fromBase(a * (M_PER_UNIT[unit] || M_PER_UNIT.ft) ** 2, s.id), s.dec) + ' ' + s.mark;
return grouped(a, d) + ' ' + unitOf(unit).area;
}
Bearing text
64-bit
Writes a direction as a quadrant bearing in degrees, minutes and seconds.
azimuth = (90 − a·180 ÷ π) mod 360; quadrant angle to whole seconds
The angle is rounded once, to whole seconds as a total count, and then split into degrees, minutes and seconds, so 59.6 seconds carries into the minutes instead of printing 60. A bearing therefore shows the direction to 1 second of arc.
units.js
lines 143 to 150
CivilCraft 0.8.1
/** Bearing text for a direction angle (radians, math convention: 0 = east, CCW) → N 45°12'30" E. */
export function bearing(a) {
let az = (90 - a * 180 / Math.PI) % 360; // azimuth from north, clockwise
if (az < 0) az += 360;
let q, ang;
if (az <= 90) { q = ['N', 'E']; ang = az; } else if (az <= 180) { q = ['S', 'E']; ang = 180 - az; } else if (az <= 270) { q = ['S', 'W']; ang = az - 180; } else { q = ['N', 'W']; ang = 360 - az; }
return q[0] + ' ' + dms(ang) + ' ' + q[1];
}
Degrees, minutes, seconds
64-bit
Splits an angle in degrees into whole degrees, minutes and seconds.
s = round(|deg| × 3600); d = floor(s ÷ 3600); m = floor(rest ÷ 60)
Rounds to whole seconds first and then works in whole numbers, so the parts always add up to the rounded total.
units.js
lines 151 to 156
CivilCraft 0.8.1
export function dms(deg) {
let s = Math.round(Math.abs(deg) * 3600);
const d = Math.floor(s / 3600); s -= d * 3600;
const m = Math.floor(s / 60); s -= m * 60;
return d + '°' + String(m).padStart(2, '0') + "'" + String(s).padStart(2, '0') + '"';
}
In detail
Method
Feet and inches as feet + inches ÷ 12; fractions as whole + numerator ÷ denominator; station as whole stations × station length + remainder.
Number types
64-bit numbers; factors 0.0254, 0.3048 and 1200 ÷ 3937 are exact by definition.
Tolerances
formatStation adds 1e-9 before taking the whole-station part; fixed() writes zero for values smaller than half the last shown decimal, so no negative zero is printed.
Limits
A feet project's feet are US survey feet (the file says so), but mm, cm and m typed into a feet project are divided by 0.0254 × 12, the international foot: a difference of 2 parts per million. Bearings are rounded to whole seconds.
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.