Cuts the arcs of a feature line into straight pieces for surfaces, grading, linework and DWG or DXF, and follows a line along an alignment for profile views.
How the precision is kept
Everything that needs straight lines takes the arc in pieces whose sagitta is at most 0.01 project units, with at least one piece per 5° and at most 720 pieces. The vertices are on the arc to 64-bit rounding; the chords are within that tolerance. Crossings with alignment transitions are found against chords of the transition.
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.
How many straight pieces draw an arc within the tolerance.
step = min(2·acos(1 − tol ÷ r), 5°); pieces = ⌈|sweep| ÷ step⌉, from 1 to 720
For a radius much larger than the tolerance, 1 − tol ÷ r is close to 1 and the acos of it loses digits; the result is only used to pick a whole number of pieces. 1e-9 is subtracted before rounding up so that an exact multiple does not gain a piece.
featureline.js
lines 128 to 134
CivilCraft 0.8.1
/** How many straight pieces draw an arc within `tol` (the sagitta), at least one per `maxDeg` degrees, at most 720. */
export function arcPieces(A, tol = 0.01, maxDeg = 5) {
if (!A) return 1;
const byTol = A.r > tol ? 2 * Math.acos(1 - tol / A.r) : Math.PI;
const step = Math.min(byTol, (maxDeg * Math.PI) / 180);
return Math.max(1, Math.min(720, Math.ceil(Math.abs(A.sweep) / step - 1e-9)));
}
Line as straight pieces
64-bit
The feature line with every arc replaced by vertices on the arc.
p = c + r·(cos θ, sin θ), θ = a₀ + sweep × s ÷ k; z = z_a + (z_b − z_a) × s ÷ k
Inner vertices are computed from the arc's center; the arc's end vertices are the stored ones, copied. Angles and elevations are start + range × s ÷ k, not running sums.
featureline.js
lines 136 to 154
CivilCraft 0.8.1
/** The line as straight pieces: [{x, y, z, v}] (v = the vertex index at a real vertex, else −1); arcs in pieces within
* `tol`, z along each arc by its length (a grade is constant along a segment). A closed line ends back at its start. */
export function flatten(f, { tol = 0.01, maxDeg = 5 } = {}) {
const P = f.pts, n = P.length, m = f.closed ? n : n - 1, out = [];
if (!n) return out;
out.push({ x: P[0].x, y: P[0].y, z: P[0].z, v: 0 });
for (let i = 0; i < m; i++) {
const a = P[i], j = (i + 1) % n, b = P[j], A = arcOf(a, b, a.b);
if (A) {
const k = arcPieces(A, tol, maxDeg);
for (let s = 1; s < k; s++) {
const th = A.a0 + (A.sweep * s) / k;
out.push({ x: A.cx + A.r * Math.cos(th), y: A.cy + A.r * Math.sin(th), z: a.z + ((b.z - a.z) * s) / k, v: -1 });
}
}
out.push({ x: b.x, y: b.y, z: b.z, v: j });
}
return out;
}
Closed design line as a grading item
64-bit
The outline and the single elevation a closed design feature line passes to grading.
z = Σ vertex z ÷ number of vertices
The elevation is the plain mean of the vertex elevations. For a line with arcs the vertices are the flattened pieces, so an arc counts once per piece and weighs more than a straight side in that mean.
featureline.js
lines 273 to 284
CivilCraft 0.8.1
/** A closed design feature line as a grading item (grading.js gradedSurface): with daylight a sloped pad, without it the
* outline itself is the region and its edge the breakline. Open lines and other uses → null. */
export function featureItem(f) {
if (f.use !== 'design' || !f.closed || f.pts.length < (hasArcs(f) ? 2 : 3)) return null;
const pts = (hasArcs(f) ? flatten(f).slice(0, -1) : f.pts).map((p) => ({ x: p.x, y: p.y, z: p.z }));
const z = pts.reduce((s, p) => s + p.z, 0) / pts.length;
if (f.daylight) return { id: f.id, kind: 'feature', name: f.name, pts, z, cut: f.cut, fill: f.fill };
return {
id: f.id, kind: 'feature', name: f.name,
patch: () => ({ problems: [], region: pts, points: [], breaklines: [{ name: f.name, pts: [...pts, pts[0]] }], info: { poly: pts } }),
};
}
Feature line written to DWG or DXF
64-bit
The 3D polyline and the extended data that records which runs of pieces were arcs.
Vertices are the flattened pieces at full 64-bit precision; the bulge of each arc is stored as a number in the extended data. Another program sees the chords only.
featureline.js
lines 294 to 309
CivilCraft 0.8.1
/** → { pts: [{x,y,z}], closed, xdata, arcsLeft } (a closed line's last piece ends at the first vertex, not repeated). */
export function cadPolyline(f) {
const flat = flatten(f), pts = flat.map(({ x, y, z }) => ({ x, y, z }));
if (f.closed && pts.length > 1) pts.pop();
const items = [[1000, CAD_TAG], [1000, String(f.name || '').slice(0, 120)], [1000, (USES.includes(f.use) ? f.use : 'eg') + '|' + (f.closed ? 1 : 0)]];
let arcs = 0, left = 0;
flat.forEach((p, k) => {
if (p.v < 0 || !okBulge(f.pts[p.v].b) || k === flat.length - 1) return;
let m = 0;
while (flat[k + 1 + m] && flat[k + 1 + m].v < 0) m++;
if (arcs >= CAD_MAX_ARCS) { left++; return; }
items.push([1040, k], [1040, m], [1040, f.pts[p.v].b]);
arcs++;
});
return { pts, closed: !!f.closed, xdata: { CADCRAFT: items }, arcsLeft: left };
}
Feature line read back from DWG or DXF
64-bit
The feature line rebuilt from a polyline and its extended data.
Where the extended data names an arc, its inner piece vertices are skipped and the stored bulge is put back on the first vertex, so the arc is the original and not a fit through the pieces. The end vertices are whatever the file holds.
featureline.js
lines 310 to 330
CivilCraft 0.8.1
/** A 3D polyline's vertices + extended data → a feature line (untrusted: checked as a project file is), or null when the
* polyline was not written by CivilCraft as a feature line. Vertices: [{x, y, z}] in project units. */
export function featureFromCad(vertices, closed, xdata, units = 'ft') {
const it = xdata?.CADCRAFT;
if (!Array.isArray(it) || it[0]?.[0] !== 1000 || it[0][1] !== CAD_TAG || !Array.isArray(vertices) || vertices.length < 2) return null;
const name = it[1]?.[0] === 1000 ? String(it[1][1]) : '';
const [use, cl] = it[2]?.[0] === 1000 ? String(it[2][1]).split('|') : [];
const arcs = new Map();
for (let q = 3; q + 2 < it.length; q += 3) {
const [k, m, b] = [it[q], it[q + 1], it[q + 2]].map((x) => (x?.[0] === 1040 ? Number(x[1]) : NaN));
if (Number.isInteger(k) && Number.isInteger(m) && k >= 0 && m >= 0 && m < 10000 && okBulge(b)) arcs.set(k, { m, b });
}
const pts = [];
for (let k = 0; k < vertices.length; k++) {
const v = vertices[k], p = { x: v.x, y: v.y, z: v.z };
const a = arcs.get(k);
if (a && k + a.m + 1 <= vertices.length) { p.b = a.b; pts.push(p); k += a.m; continue; } // skip the arc's inner pieces
pts.push(p);
}
return cleanFeature({ name, pts, closed: cl === '1' || (cl === undefined && !!closed), use }, units);
}
Crossings with an alignment
64-bit
Every place a feature line crosses the centerline, with its station and elevation.
z = z_a + t × Δz
Lines and circular arcs of the alignment are crossed exactly; transitions are crossed as chords of at most 0.25° of turn, so a crossing on a transition is on the chord. Two crossings within 1e-6 in both station and elevation are merged into one.
featureline.js
lines 402 to 414
CivilCraft 0.8.1
/** Every crossing of a feature line with an alignment's centerline (see the header). */
export function alignmentCrossings(al, f) {
if (!al?.elements?.length || !f?.pts?.length) return [];
const out = [];
for (const sg of segments(f)) {
if (!(sg.len > 1e-12)) continue;
const a = f.pts[sg.i], b = f.pts[sg.j];
for (const e of al.pieces()) for (const c of sg.arc ? crossArcElement(e, sg.arc) : crossElement(e, a, b)) out.push({ sta: c.sta, z: a.z + c.t * sg.dz, i: sg.i, grade: sg.grade });
}
out.sort((p, q) => p.sta - q.sta);
// a crossing exactly at a shared vertex / element joint is found twice
return out.filter((c, k) => !k || Math.abs(c.sta - out[k - 1].sta) > 1e-6 || Math.abs(c.z - out[k - 1].z) > 1e-6);
}
Stretches running beside an alignment
64-bit
The parts of a feature line within a given distance of the centerline, as station and elevation for a profile view.
Each vertex is projected to its nearest station and offset. A line with arcs is followed through its flattened pieces. Between vertices the profile joins the projected points with straight lines, so the run is known at the vertices only.
featureline.js
lines 416 to 438
CivilCraft 0.8.1
/** The line's stretches within `within` of the centerline, as profile polylines by station (see the header). */
export function projectedRuns(al, f, within) {
if (!(within > 0) || !al?.elements?.length || !f?.pts?.length) return [];
// 0.6 WL.24: a line with arcs is followed through its pieces (within 0.01 of the curve)
if (hasArcs(f)) return projectedRuns(al, { ...f, pts: flatten(f).map(({ x, y, z }) => ({ x, y, z })), closed: false }, within);
const n = f.pts.length, idx = [...Array(n).keys()];
if (f.closed) idx.push(0);
const cross = alignmentCrossings(al, f);
const runs = [];
let run = [];
// a stretch with no length along the alignment (a line straight across it) is only its crossing: no run
const flush = () => { if (run.length >= 2 && Math.max(...run.map((q) => q.sta)) - Math.min(...run.map((q) => q.sta)) > 1e-6) runs.push(run); run = []; };
for (let k = 0; k < idx.length; k++) {
const p = f.pts[idx[k]], so = al.stationOffset(p.x, p.y);
const inside = so && !so.outside && Math.abs(so.off) <= within + 1e-9;
if (!inside) { flush(); continue; }
if (k > 0 && run.length) for (const c of cross) if (c.i === idx[k - 1]) run.push({ sta: c.sta, z: c.z, off: 0 });
run.push({ sta: so.sta, z: p.z, off: so.off });
}
flush();
// a run is read along the alignment: keep its points in station order when the line runs backwards
return runs.map((r) => (r[r.length - 1].sta < r[0].sta ? r.reverse() : r));
}
In detail
Method
Pieces per arc from the sagitta: step = 2·acos(1 − tol ÷ r), capped at 5°. Elevation along an arc changes evenly with length. In a drawing the pieces are written with extended data that records each arc's bulge, so CivilCraft restores the arc itself.
Number types
64-bit numbers.
Tolerances
Sagitta 0.01 project units, the same number in a feet or a meter project. At least one piece per 5°, at most 720 pieces per arc. A run within the band needs a station range above 1e-6. Band width + 1e-9.
Limits
With the 720-piece cap an arc of very large radius and long sweep can exceed the 0.01 sagitta. Extended data holds at most 500 arcs per line; arcs past that stay as pieces when read back.
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.