Resources Numerical accuracy Calculation policy

Plan drawing and section sheets for DXF and PDF

Turns the model into the linework and labels of the plan drawing and the cross-section sheets, as they are drawn on screen and written to DXF and PDF.

How the precision is kept

Model geometry is passed through in 64-bit model coordinates without rounding: contours, breaklines, pads and alignment lines are copied, and alignment curves are written as true arcs from their center, radius and angles. Numbers are rounded only when they become label text. Section sheets map offsets and elevations to paper millimeters with one multiply and add per point.

Calculations under this policy

7 calculations in 2 source files. 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.

scene.js CivilCraft 0.8.1 64-bit 4 calculations Reviewed

Tests bin/test-civilcraft.mjs, bin/test-civilcraft-cad.mjs, bin/test-civilcraft-sheets.mjs

Major contour test

64-bit

Decides whether a contour goes on the major layer and gets an elevation label.

major when |z ÷ M − round(z ÷ M)| < 1e-6, M = interval × major count

The test is on the ratio z ÷ M, so the 1e-6 is relative to the major interval, not to the elevation.

  • scene.js
  • lines 67 to 69
  • CivilCraft 0.8.1
const major = C.interval * (C.major || 5);
const isMajor = (z) => Math.abs(z / major - Math.round(z / major)) < 1e-6;
const dec = decimalsOf(C.interval);

Alignment curves as arcs

64-bit

Writes each curve of an alignment as an arc with its center, radius and start and end angles.

a₁ = a₀ ± Δ; arc from a₀ to a₁ counterclockwise

The center, radius and start angle are copied from the alignment element and the end angle is one 64-bit addition, so the arc is not turned into chords here. Spirals are written as polylines sampled at π ÷ 720 radians.

  • scene.js
  • lines 113 to 120
  • CivilCraft 0.8.1
for (const e of al.elements) {
  if (e.type === 'line') items.push({ layer: 'C-ROAD-CNTR', kind: 'poly', pts: [e.p0, e.p1] });
  else if (e.type === 'spiral') items.push({ layer: 'C-ROAD-CNTR', kind: 'poly', pts: al.polyline(Infinity, Math.PI / 720).filter((q) => q.sta >= e.sta - 1e-9 && q.sta <= e.sta + e.len + 1e-9).map((q) => ({ x: q.x, y: q.y })) });   // 0.7 D7.35
  else {
    const a1 = e.a0 + (e.ccw ? e.delta : -e.delta);
    items.push({ layer: 'C-ROAD-CNTR', kind: 'arc', c: e.c, r: e.r, a0: e.ccw ? e.a0 : a1, a1: e.ccw ? a1 : e.a0 });
  }
}

Pipe line between structure walls

64-bit

Draws each pipe from the wall of one structure to the wall of the next.

round: half = outside width ÷ 2; rectangle: half = min(L ÷ 2 ÷ |u·along|, W ÷ 2 ÷ |u·across|)

For a rectangular structure the distance to the wall is the smaller of two quotients, each skipped when its divisor is 1e-12 or less. Each end is kept at least 1.2 mm of paper from the structure's center, so on a small-scale plot the drawn pipe end is a drawing convention, not the wall.

  • scene.js
  • lines 163 to 172
  • CivilCraft 0.8.1
const half = (x, dx, dy) => {
  const g = structGeom(x, net, u);
  if (!g.type) return (x.dia || 0) / per / 2;
  if (g.shape !== 'rect') return g.outW / 2;
  const out = net.pipes.find((q) => q.from === x.id), to = out && byId.get(out.to), ang = to ? Math.atan2(to.y - x.y, to.x - x.x) : 0;
  const lx = Math.abs(dx * Math.cos(ang) + dy * Math.sin(ang)), ly = Math.abs(-dx * Math.sin(ang) + dy * Math.cos(ang));
  return Math.min(lx > 1e-12 ? g.outL / 2 / lx : Infinity, ly > 1e-12 ? g.outW / 2 / ly : Infinity);
};
const ra = Math.max(half(a, ux, uy), 1.2 * unitPerMm), rb = Math.max(half(b, -ux, -uy), 1.2 * unitPerMm);
const p0 = { x: a.x + ux * ra, y: a.y + uy * ra }, p1 = { x: b.x - ux * rb, y: b.y - uy * rb };

Scene to DXF entities

64-bit

Turns the scene's lines, arcs, points and labels into the entities the DXF writer takes.

text height = paper mm ÷ (mm per model unit); middle-anchored text moved by height ÷ 2 across its direction

Coordinates, elevations, radii and angles are passed to the DXF writer unchanged. Only text is computed here: its height is one division and its insertion point is shifted by half the height.

  • scene.js
  • lines 235 to 250
  • CivilCraft 0.8.1
/** Scene → DXF entities (text height from paper mm through the plot scale). */
export function toDxfEntities(scene, mmPerUnit) {
  const out = [];
  for (const it of scene.items) {
    if (it.kind === 'poly') out.push(it.z ? { type: 'poly3d', layer: it.layer, pts: it.pts, closed: it.closed, ...(it.xdata ? { xdata: it.xdata } : {}) } : { type: 'poly', layer: it.layer, pts: it.pts, closed: it.closed, elev: it.elev || 0 });
    else if (it.kind === 'arc') out.push({ type: 'arc', layer: it.layer, c: it.c, r: it.r, a0: it.a0, a1: it.a1 });
    else if (it.kind === 'point') out.push({ type: 'point', layer: it.layer, x: it.x, y: it.y, z: it.z });
    else if (it.kind === 'text') {
      const h = it.mm / mmPerUnit;
      // DXF text sits on its baseline; move a middle-anchored label down by half its height
      const dx = it.middle ? Math.sin(it.rot) * h / 2 : 0, dy = it.middle ? -Math.cos(it.rot) * h / 2 : 0;
      out.push({ type: 'text', layer: it.layer, x: it.x + dx, y: it.y + dy, h, rot: it.rot, text: it.text, align: it.align });
    }
  }
  return out;
}
section-sheets.js CivilCraft 0.8.1 64-bit 3 calculations Reviewed

Tests bin/test-civilcraft-sections.mjs

Section cell size and vertical exaggeration

64-bit

Sizes the cell every section is drawn in and picks the largest vertical exaggeration that fits the page.

width = (left + right)·s + margins; height = 1.2 × largest relief × s × vex + margins; vex = the largest of 10, 5, 2, 1 not above the wanted one that fits

64-bit sizes in paper millimeters. The number of columns and rows is a floor of the available size over the cell size. When the exaggeration is lowered the sheet says so in a note.

  • section-sheets.js
  • lines 36 to 46
  • CivilCraft 0.8.1
const cw = (left + right) * s + padX + 4;
if (cw > area.x1 - area.x0) notes.push(t('xs.wide'));
const fitV = (v) => maxRel * 1.2 * s * v + padTop + padBot <= area.y1 - area.y0;
const want = SHEET_VEXES.includes(vex) ? vex : 5;
const used = SHEET_VEXES.filter((v) => v <= want).find(fitV) ?? 1;
if (used !== want) notes.push(t('xs.vex', { want, used }));
const sv = s * used;
const ch = maxRel * 1.2 * sv + padTop + padBot;
const cols = Math.max(1, Math.floor((area.x1 - area.x0) / cw)), rows = Math.max(1, Math.floor((area.y1 - area.y0) / ch));
const perPage = cols * rows;
const pages = Math.ceil(secs.length / perPage);

Vertical scale label

64-bit

The vertical scale printed in the title strip of a section sheet.

m: 1 : round(1000 ÷ sv); ft: 1 inch = (25.4 ÷ sv) feet, to 1 decimal

The label is rounded text: a whole number for the metric ratio and one decimal for feet per inch. 25.4 mm per inch is exact. The drawing itself uses the unrounded scale.

  • section-sheets.js
  • lines 49 to 50
  • CivilCraft 0.8.1
const hLabel = scale.label, vLabel = units === 'm' ? '1:' + Math.round(1000 / sv) : '1" = ' + String(+(25.4 / sv).toFixed(1)) + "'";
const areaU = unitOf(units).area;

Section points on paper

64-bit

Places each section's ground and design points, grid lines and labels in its cell.

X = x₀ + (o + left)·s; Y = y₀ + (z − z₀)·sv; z₀ = floor((z_mid − span ÷ 2) ÷ step)·step

Each point is one subtraction, multiply and add in 64-bit. The grid lines step by repeated addition of the grid step, which only affects where grid lines and their rounded labels fall.

  • section-sheets.js
  • lines 58 to 73
  • CivilCraft 0.8.1
const r = rel[pg * perPage + k] || [0, 1], zmid = (r[0] + r[1]) / 2, zSpan = (cy1 - cy0) / sv;
const z0 = Math.floor((zmid - zSpan / 2) / zStep) * zStep;
const X = (o) => cx0 + (o + left) * s, Y = (z) => cy0 + (z - z0) * sv;
const xr = X(right);
// grid
for (let o = Math.ceil(-left / oStep) * oStep; o <= right + 1e-9; o += oStep) {
  strokes.push({ color: GRID, width: 0.08, pts: [{ x: X(o), y: cy0 }, { x: X(o), y: cy1 }] });
  texts.push({ color: INK, x: X(o), y: cy0 - 2.6, h: 1.3, rot: 0, align: 'center', text: (o > 0 ? '+' : '') + fixed(o, 0) });
}
for (let z = z0; z <= z0 + zSpan + 1e-9; z += zStep) {
  const y = Y(z);
  if (y > cy1 + 1e-6) break;
  strokes.push({ color: GRID, width: 0.08, pts: [{ x: cx0, y }, { x: xr, y }] });
  texts.push({ color: INK, x: cx0 - 1, y, h: 1.3, rot: 0, align: 'right', middle: true, text: fixed(z, zStep < 1 ? 1 : 0).replace(/,/g, '') });
}
strokes.push({ color: INK, width: 0.25, pts: [{ x: cx0, y: cy0 }, { x: xr, y: cy0 }, { x: xr, y: cy1 }, { x: cx0, y: cy1 }], closed: true });

In detail

Method
Scene items in model coordinates; text heights in paper millimeters divided by the plot scale; section cells at the plan scale across and scale × vertical exaggeration up.
Number types
64-bit numbers.
Tolerances
A contour is a major contour when its elevation ÷ major interval is within 1e-6 of a whole number; a station tick is major within 1e-9; daylight rays and slope ticks shorter than 1e-6 are not drawn; pipes shorter than 1e-9 are skipped; sections are placed on a sheet when their station is within 1e-9 of the range.
Limits
Structures drawn as circles are 24-sided polygons. Spirals are drawn as polylines with a step of π ÷ 720 radians. Labels are rounded text.

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.

Every policy that takes this approach