Works out a horizontal alignment from its points of intersection: tangents, circular curves, curve facts, stations, and the station and offset of any point.
How the precision is kept
Lines and arcs are closed-form 64-bit expressions from the PI coordinates and radius. Stations accumulate by adding element lengths in order. The comparisons use small absolute tolerances (1e-9 to 1e-12) that are not scaled to the size of the coordinates.
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.
Brings a direction difference into one turn, so a deflection is read the short way round.
One remainder by 2π (the 64-bit value of Math.PI × 2) and at most one addition. The deflection at every PI passes through it.
alignment.js
line 37
CivilCraft 0.8.1
export const normAng = (a) => { a %= TAU; if (a <= -Math.PI) a += TAU; if (a > Math.PI) a -= TAU; return a; };
Deflection, tangent lengths and curve center at a PI
64-bit
The deflection angle at each PI, the tangent length of a plain curve, and for a curve with transitions the center and the two tangent lengths.
Δ = a₂ − a₁; T = R·tan(Δ/2); center from n₁·c = R + p₁, n₂·c = R + p₂; T₁ = k₁ − u₁·c, T₂ = u₂·c + k₂
Directions come from atan2 of coordinate differences. The 2×2 solve for the center divides by the cross product of the two normals, which is sin Δ; deflections below 1e-9 rad or within 1e-6 rad of π are excluded before it, so the divisor cannot be zero.
The arc's center, start angle, length, external, middle ordinate and chord, and the PC and PT stations.
L = R·Δ; E = R·(1 ÷ cos(Δ/2) − 1); M = R·(1 − cos(Δ/2)); chord = 2R·sin(Δ/2)
E and M each subtract two numbers that are nearly equal when the deflection is small, so they carry fewer digits than L and the chord for a flat curve. The PT is laid off from the PI along the outgoing tangent, the center from the PC along the normal.
The circular arc left between two transitions and the stations TS, SC, CS and ST.
Δc = |Δ| − θ₁ − θ₂; Lc = R·Δc
The arc is built forward from the end of the entry transition; the exit transition is built backward from the ST, which is laid off from the PI. The arc's end and the exit transition's start are therefore computed by two separate paths and are not forced to the same coordinates. An arc with Δc of 1e-12 rad or less is left out.
The coordinates and forward direction on the alignment at a station.
line: p₀ + s·(cos dir, sin dir); arc: c + R·(cos a, sin a), a = a₀ ± s ÷ R
The distance into the element is the station minus the element's start station, a difference of two running sums. On an arc the point is computed from the center, so it stays on the circle to rounding.
The nearest station on the alignment to a point, and how far left or right of it the point lies.
line: s = (p − p₀)·u clamped to the element; arc: s = R × swept angle; offset = (p − foot) × direction
Lines project by a dot product, arcs by atan2 about the center; transitions go through a sampled search. Between elements the nearer foot wins by more than 1e-12 (absolute). The offset is a cross product of a short difference with a unit direction.
alignment.js
lines 267 to 300
CivilCraft 0.8.1
/** Nearest station and offset (right = +). `outside` is true when the point is beyond either end. */
stationOffset(x, y) {
let best = null;
for (const e of this.elements) {
let s, px, py, dir;
if (e.type === 'line') {
const ux = Math.cos(e.dir), uy = Math.sin(e.dir);
s = Math.max(0, Math.min(e.len, (x - e.p0.x) * ux + (y - e.p0.y) * uy));
px = e.p0.x + ux * s; py = e.p0.y + uy * s; dir = e.dir;
} else if (e.type === 'spiral') {
const q = spiralNearest(e, x, y); // 0.7 D7.35
s = q.s; px = q.x; py = q.y; dir = q.dir;
} else {
const a = Math.atan2(y - e.c.y, x - e.c.x);
let sw = ((a - e.a0) * (e.ccw ? 1 : -1)) % TAU;
if (sw < 0) sw += TAU;
if (sw > e.delta) sw = sw - e.delta < TAU - sw ? e.delta : 0; // off the arc: its nearer end
s = sw * e.r;
const aa = e.a0 + (e.ccw ? sw : -sw);
px = e.c.x + Math.cos(aa) * e.r; py = e.c.y + Math.sin(aa) * e.r; dir = aa + (e.ccw ? Math.PI / 2 : -Math.PI / 2);
}
const d = Math.hypot(x - px, y - py);
if (!best || d < best.d - 1e-12) {
const off = (x - px) * Math.sin(dir) - (y - py) * Math.cos(dir); // + to the right
best = { d, sta: e.sta + s, off, x: px, y: py, dir };
}
}
if (!best) return null;
// beyond the ends: the projection is not perpendicular
const along = (p, dir) => (x - p.x) * Math.cos(dir) + (y - p.y) * Math.sin(dir);
const p0 = this.pointAt(this.start), p1 = this.pointAt(this.end);
best.outside = (best.sta <= this.start + 1e-9 && along(p0, p0.dir) < -1e-9) || (best.sta >= this.end - 1e-9 && along(p1, p1.dir) > 1e-9);
return { sta: best.sta, off: best.off, x: best.x, y: best.y, outside: best.outside, dist: best.d };
}
Station ticks
64-bit
The points at every whole multiple of the station interval.
station = k × step, k whole
Each station is a whole number times the step, not a running sum, so tick stations do not drift. The first and last k are found with a 1e-9 allowance; more than 20,000 ticks returns none.
alignment.js
lines 313 to 320
CivilCraft 0.8.1
ticks(step) {
if (!(step > 0) || !this.elements.length) return [];
const out = [];
const k0 = Math.ceil(this.start / step - 1e-9), k1 = Math.floor(this.end / step + 1e-9);
if (k1 - k0 > 20000) return [];
for (let k = k0; k <= k1; k++) { const sta = k * step; const p = this.pointAt(sta); out.push({ sta, ...p }); }
return out;
}
In detail
Method
Arc-definition curve formulas, as the file header states: T = R·tan(Δ/2), L = R·Δ, E = R(sec(Δ/2) − 1), M = R(1 − cos(Δ/2)), chord = 2R·sin(Δ/2). With transitions, the circle center is the intersection of two lines offset R + p from the tangents.
Number types
64-bit numbers throughout.
Tolerances
Repeated PI: closer than 1e-9. Angle point: deflection below 1e-9 rad. Reversal: deflection within 1e-6 rad of π. Transitions too long: θ₁ + θ₂ above Δ + 1e-12. Tangent overlap: need above length + 1e-9. Elements shorter than 1e-9 are dropped. Element lookup by station: 1e-9. All absolute.
Limits
E and M subtract nearly equal numbers for a small deflection. Stations are a running sum of element lengths, so rounding accumulates along a long alignment.
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.