Sweeps a road or track assembly along an alignment and profile: section stations, edge and shoulder points, targets, daylight points, and the area and volume of pavement courses.
How the precision is kept
Each section point is a 64-bit offset from the centerline point at a cross slope. The corridor exists only at its section stations, so the surface and the quantities depend on the station step. Stations from the step, the superelevation plan and the cant plan are rounded to 6 decimals.
Calculations under this policy
6 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.
s = ⌈from ÷ step⌉·step, then s += step; each rounded to 6 decimals
Step stations are a running sum, and each is rounded to 6 decimals with toFixed before it is kept, which removes the drift of the sum. Alignment key stations and vertical curve ends are kept unrounded. Stations within 1e-6 of the one before are dropped.
corridor.js
lines 246 to 258
CivilCraft 0.8.1
const st = new Set([from, to]);
const step = A.step > 0 ? A.step : 10;
for (let s = Math.ceil(from / step) * step; s < to; s += step) st.add(Number(s.toFixed(6)));
for (const k of alignment.keyStations()) if (k.sta > from && k.sta < to) st.add(k.sta);
for (const c of profile.curves) for (const s of [c.bvc, c.evc]) if (s > from && s < to) st.add(s);
const railOn = !!A.rail?.on; // 0.7.1 71.26
const rp = railOn ? cantPlan(alignment, A.rail, { speed: A.rail.speed, minSpeed: A.rail.minSpeed, units, id: A.rail.id, name: A.rail.name }) : null;
if (rp) for (const s of rp.stations) if (s > from && s < to) st.add(Number(s.toFixed(6)));
const custom = !railOn && !!(A.parts && (A.parts.L?.length || A.parts.R?.length)); // 0.7 D7.35
const nc = custom ? ncOf(A) : A.laneSlope;
const sp = !railOn && A.super?.on ? superPlan(alignment, { super: A.super, nc, lane: custom ? laneRunWidth(A) : A.lane }) : null; // 0.4 V18
if (sp) { for (const s of superStations(sp)) if (s > from && s < to) st.add(Number(s.toFixed(6))); problems.push(...sp.problems); }
const stations = [...st].sort((a, b) => a - b).filter((s, i, a) => !i || s - a[i - 1] > 1e-6);
Lane edge and shoulder point (simple assembly)
64-bit
The edge of pavement and the shoulder's outer edge on one side of one section, with any width or elevation target applied.
edge z = z₀ − lane width × slope; shoulder z = edge z − shoulder width × shoulder slope
Each point is the centerline point plus a width times a unit vector, so its error is that of the full-size coordinate it is added to. With an elevation target the lane slope is recomputed as (z₀ − target z) ÷ width; a width of 1e-9 or less keeps the assembly's slope instead of dividing.
Every point of a side from the centerline out: lanes, gutter, curb, strip, walk, then the daylight point.
t += width; z −= width × slope (curb: z += height)
Offset and elevation are running sums over the parts, at most 12 of them. A curb face is moved out by 0.01 ft or 0.003 m so it is not vertical. With an elevation target on the pavement edge, lanes already placed are given new elevations from their distance to the centerline.
corridor.js
lines 184 to 234
CivilCraft 0.8.1
/**
* 0.7 D7.35: one side of one section from its parts. Returns { det, eop, sh, dl }: det = every point from the centreline out
* (each with its `code`), eop = the end of the lanes in a row from the centre, sh = the last point before the daylight, dl = the
* daylight point. c.tg(what, part) is the side's target hit (corridor.js `hitOf`); c.dsl = the superelevation's change to a lane's slope.
*/
function walkSide(parts, c) {
const { p, ux, uy, z0, cl, dsl, A, surface, step, far, eps, tg } = c;
const day = parts.find((q) => q.type === 'daylight');
const body = parts.filter((q) => q.type !== 'daylight');
let run = 0;
while (run < body.length && body[run].type === 'lane') run++;
const hW = tg('width', 'eop'), hE = tg('elev', 'eop'), hSW = tg('width', 'sh'), hSE = tg('elev', 'sh');
let lw = -1; // the last part with a width (the shoulder's target moves it)
body.forEach((q, i) => { if (q.type !== 'curb' && q.w > 1e-9) lw = i; });
const det = [cl], seen = {};
const code = (base) => { seen[base] = (seen[base] || 0) + 1; return seen[base] > 1 ? base + seen[base] : base; };
let t = 0, z = z0, eop = cl, prev = null;
const push = (cd) => { const q = { x: p.x + ux * t, y: p.y + uy * t, z, code: cd }; det.push(q); return q; };
body.forEach((q, i) => {
if (q.type === 'curb') {
t += eps; z += q.h; push(code('TOC'));
t += q.w; push(code('BOC'));
if (q.drop > 0) { t += eps; z -= q.drop; push(code('BC')); }
return;
}
let w = q.w;
if (hW && i === run - 1) w = Math.max(0, hW.t - t);
if (hSW && i === lw) w = Math.max(0, hSW.t - t);
if (!(w > 1e-9)) return;
let slope = q.slope;
if (q.type === 'lane') slope = q.slope + dsl;
else if (q.type === 'shoulder' && i === run && prev !== null) slope = prev > q.slope ? prev : q.slope; // 0.4 V18: the low side's shoulder no flatter than its lane
if (q.type === 'lane' && hE && i === run - 1) {
// the edge of pavement takes the line's elevation: every lane in the run falls at the one slope that gets there
const total = t + w;
slope = total > 1e-9 ? (z0 - hE.z) / total : slope;
z = z0 - t * slope; // (the lanes before this one are re-laid at the same slope)
for (const d of det) if (d.code && /^LANE/.test(d.code)) d.z = z0 - Math.hypot(d.x - p.x, d.y - p.y) * slope;
}
t += w; z -= w * slope;
const pt = push(q.type === 'lane' ? (i === run - 1 ? 'EOP' : code('LANE')) : code(PART_CODES[q.type]));
if (q.layers?.length) { pt.part = q.type; pt.layers = q.layers; } // 0.7.1 71.24: the courses under the stretch that ends here
if (q.type === 'lane') { prev = slope; if (i === run - 1) eop = pt; }
if (hSE && i === lw) { z = hSE.z; pt.z = z; }
});
const sh = det[det.length - 1];
const r = castSlope(surface, sh, ux, uy, sh.z, day?.cut || A.cut, day?.fill || A.fill, { step: Math.max(0.1, step / 20), far });
const dl = { x: r.x, y: r.y, z: r.z, kind: r.kind, code: 'DL' };
det.push(dl);
return { det, eop, sh, dl };
}
Where a section meets a target line
64-bit
The distance out along a section to the first crossing of a 3D polyline, and the line's elevation there.
t = (w × d) ÷ (u × d); z = z_a + s·(z_b − z_a)
A 2D ray and segment intersection by cross products. The parallel test compares the denominator with 1e-15 as an absolute number, so it is not scaled to the segment length; a crossing within 1e-9 of the centerline is ignored. The elevation is interpolated along the crossed segment in plan.
corridor.js
lines 169 to 182
CivilCraft 0.8.1
/** Where the ray from p along (ux, uy) first crosses the 3D polyline `line` (t > 0, the distance out), with the line's z
* there (by length along the crossed segment). */
export function sectionHit(p, ux, uy, line) {
let best = null;
for (let i = 1; i < (line?.length || 0); i++) {
const a = line[i - 1], b = line[i], dx = b.x - a.x, dy = b.y - a.y, den = ux * dy - uy * dx;
if (Math.abs(den) < 1e-15) continue;
const wx = a.x - p.x, wy = a.y - p.y;
const t = (wx * dy - wy * dx) / den, u = (wx * uy - wy * ux) / den;
if (t <= 1e-9 || u < -1e-12 || u > 1 + 1e-12) continue;
if (!best || t < best.t) best = { t, z: a.z + Math.min(1, Math.max(0, u)) * (b.z - a.z) };
}
return best;
}
Do two sections cross
64-bit
Whether neighboring sections intersect, which happens inside a curve tighter than the corridor is wide.
Four orientation cross products compared with ±1e-12. The products are formed from full-size project coordinates, whose own rounding is far larger than 1e-12, so the test is in effect a sign test and is not an exact predicate.
The plan area and the volume of each pavement course along the corridor.
area = Σ (w₁ + w₂) ÷ 2 × Δstation; volume = area × thickness
Widths are plan distances between the inner and outer edge points by hypot. The area is one running sum over the sections. Stations are measured along the centerline, so on a curve the course's own length differs from the station difference; the code makes no correction for that.
corridor.js
lines 365 to 378
CivilCraft 0.8.1
/** 0.7.1 71.24: area (plan) and volume of every course: the part's plan width between neighbouring sections, averaged × the
* station step (as the sections' average end areas), × the course thickness. */
export function courseQuantities(info) {
const out = [];
for (const c of info?.courses || []) {
let area = 0;
for (let k = 1; k < c.edges.length; k++) {
const a = c.edges[k - 1], b = c.edges[k], w = (e) => Math.hypot(e.o.x - e.i.x, e.o.y - e.i.y);
area += ((w(a) + w(b)) / 2) * (b.sta - a.sta);
}
c.layers.forEach((L, k) => out.push({ side: c.side, code: c.code, part: c.part, k, name: L.name, material: L.material, course: L.course, t: L.t, area, volume: area * L.t, length: c.edges[c.edges.length - 1].sta - c.edges[0].sta }));
}
return out;
}
In detail
Method
Sections square to the alignment at every step, at alignment key stations and at vertical curve ends. Points are laid out by width and cross slope from the centerline. Course quantities by average width between neighboring sections times the station difference times the thickness.
Number types
64-bit numbers.
Tolerances
Station step values rounded to 6 decimals; stations closer than 1e-6 merged. Target crossing: denominator below 1e-15 treated as parallel, crossings nearer than 1e-9 ignored, segment parameter allowed ±1e-12. Widths of 1e-9 or less are skipped. Section crossing test: cross products beyond ±1e-12. All absolute.
Limits
Between sections the surface is whatever the triangulation draws through the section points. The daylight point comes from a stepped search in another module (castSlope in grading.js). A curb face is offset 0.01 ft or 0.003 m so that it is not vertical.
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.