Turns the lines, arcs, polylines, splines, ellipses and block references of a drawing into linework, feature lines and survey points in the project's units.
How the precision is kept
Coordinates stay 64-bit from the drawing model to the project. Curves are replaced by vertices at most 5° of turn apart, and splines by evenly spaced samples, so a curve's imported shape is a chain of chords. The unit scale is one 64-bit division of two decimal factors.
Calculations under this policy
15 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.
The length of each $INSUNITS drawing unit in meters.
Each factor is the unit's defined value: 0.0254, 0.3048, 1609.344 and 0.9144 are exact by definition, and the US survey foot is written as the fraction 1200 / 3937.
The factor every imported coordinate is multiplied by.
inch, yard, mile into feet: meters ÷ 0.3048; metric into feet: meters ÷ (1200 ÷ 3937); into meters: meters ÷ 1
The scale is one 64-bit division of two decimal factors, not a whole-number fraction. For inches, yards and miles into feet the quotient equals 1/12, 3 and 5280 exactly in 64-bit (checked for this review). A drawing in feet goes into a feet project at scale 1 whether its feet are international or US survey; the two differ by 2 parts per million and the import note says so. The scale shown in that note is rounded to 10 digits; the scale applied is not.
cad-linework.js
lines 69 to 80
CivilCraft 0.8.1
/** Drawing units → project units ('ft' = US survey feet, 'm'). */
export function unitScale(header, projectUnit = 'ft') {
const u = cadUnits(header);
const to = projectUnit === 'm' ? 'm' : 'ft';
if (!u.metres) return { scale: 1, from: '', to, note: t('cad.units.unknown', { unit: t('cad.unit.' + to) }) };
if (to === 'ft' && FEET.has(u.label)) return { scale: 1, from: u.label, to, note: u.label === 'ft' ? t('cad.units.feet') : null };
if (to === 'm' && u.label === 'm') return { scale: 1, from: 'm', to, note: null };
const projM = to === 'm' ? 1 : 1200 / 3937;
// inches and other imperial lengths into a feet project: exact ratios to the international foot
const scale = to === 'ft' && ['in', 'mi', 'yd', 'mil', 'microinch'].includes(u.label) ? u.metres / 0.3048 : u.metres / projM;
return { scale, from: u.label, to, note: t('cad.units.scaled', { from: u.label, to: t('cad.unit.' + to), scale: +scale.toPrecision(10) }) };
}
Object coordinate system
64-bit
The rotation from an entity's own coordinate system to world coordinates, from its extrusion direction.
Az = N ÷ |N|; Ax = (Wy or Wz) × Az, normalized; Ay = Az × Ax
The extrusion vector is normalized with hypot. The switch between the two reference axes is at 1/64, the value the DXF reference gives. An extrusion within 1e-12 of +Z is treated as exactly +Z and no rotation is applied.
cad-linework.js
lines 94 to 106
CivilCraft 0.8.1
/** OCS → WCS for an extrusion direction (DXF reference, "Arbitrary axis algorithm"); null for the plain +Z. */
export function ocs(ext) {
if (!ext || !Number.isFinite(ext.x) || !Number.isFinite(ext.y) || !Number.isFinite(ext.z)) return null;
const L = Math.hypot(ext.x, ext.y, ext.z);
if (!L) return null;
const az = [ext.x / L, ext.y / L, ext.z / L];
if (Math.abs(az[0]) < 1e-12 && Math.abs(az[1]) < 1e-12 && az[2] > 0) return null;
const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
let ax = Math.abs(az[0]) < 1 / 64 && Math.abs(az[1]) < 1 / 64 ? cross([0, 1, 0], az) : cross([0, 0, 1], az);
const n = Math.hypot(...ax); ax = ax.map((v) => v / n);
const ay = cross(az, ax);
return [ax[0], ay[0], az[0], 0, ax[1], ay[1], az[1], 0, ax[2], ay[2], az[2], 0];
}
Composing transforms
64-bit
The product of two 3×4 affine transforms, used for nested block references.
Each entry is a sum of three products plus the translation, in 64-bit. Each block reference forms five of these products (six with an extrusion), and a reference nested 8 deep repeats that at every level before a vertex is transformed.
cad-linework.js
lines 82 to 90
CivilCraft 0.8.1
// ---------------------------------------------------------------- 3D affine transforms (row-major 3×4)
function mul(a, b) { // a ∘ b
const o = new Array(12);
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) o[r * 4 + c] = a[r * 4] * b[c] + a[r * 4 + 1] * b[4 + c] + a[r * 4 + 2] * b[8 + c];
o[r * 4 + 3] = a[r * 4] * b[3] + a[r * 4 + 1] * b[7] + a[r * 4 + 2] * b[11] + a[r * 4 + 3];
}
return o;
}
Block reference placement
64-bit
The transform of one block reference: base point, scale, array spacing, rotation, insertion point and extrusion.
Five matrix products are formed for every reference and every array cell, six when the reference has an extrusion. The array counts are clamped to 1 to 1000 with a bitwise OR on the count, which is a whole number; no coordinate is truncated.
n = max(2, ⌈sweep ÷ 5°⌉); p = c + r·(cos a, sin a)
Every vertex is on the circle to rounding; the chord between two vertices is not. Angles are start + sweep × i ÷ n, not a running sum, so the last vertex lands on the end angle.
cad-linework.js
lines 109 to 117
CivilCraft 0.8.1
// ---------------------------------------------------------------- curves → vertices
function arcPts(cx, cy, r, a0, a1, z = 0) { // CCW from a0 to a1 (radians)
let sweep = a1 - a0;
while (sweep <= 0) sweep += 2 * Math.PI;
const n = Math.max(2, Math.ceil(sweep / STEP));
const out = [];
for (let i = 0; i <= n; i++) { const a = a0 + (sweep * i) / n; out.push([cx + r * Math.cos(a), cy + r * Math.sin(a), z]); }
return out;
}
Polyline bulges as vertices
64-bit
The arc between two polyline vertices that carry a bulge, as intermediate points.
θ = 4·atan(bulge); r = chord ÷ (2·sin(θ/2)); center = chord middle + r·cos(θ/2) along the normal
An arc of 5° or less adds no intermediate vertices, so points are only ever evaluated from a center that is at most about 11.5 chord lengths away. Elevation along the arc is interpolated linearly by step.
cad-linework.js
lines 118 to 144
CivilCraft 0.8.1
/** Vertices with DXF bulges (the arc starting at a vertex) → points; `closed` adds the closing segment's arc. */
function bulgedPts(vs, closed, zOf) {
const out = [];
const n = vs.length;
for (let i = 0; i < n; i++) {
const a = vs[i], z = zOf(a);
out.push([a.x, a.y, z]);
if (i === n - 1 && !closed) break;
const b = vs[(i + 1) % n], bu = a.bulge || 0;
if (!bu || !Number.isFinite(bu)) continue;
const dx = b.x - a.x, dy = b.y - a.y, c = Math.hypot(dx, dy);
if (!c) continue;
const theta = 4 * Math.atan(bu); // signed sweep
const r = c / (2 * Math.sin(theta / 2));
const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
const h = r * Math.cos(theta / 2); // centre distance from the chord's middle (signed with r)
const cx = mx - (dy / c) * h, cy = my + (dx / c) * h;
const a0 = Math.atan2(a.y - cy, a.x - cx);
const steps = Math.max(1, Math.ceil(Math.abs(theta) / STEP));
const zb = zOf(b);
for (let k = 1; k < steps; k++) {
const ang = a0 + (theta * k) / steps, R = Math.abs(r);
out.push([cx + R * Math.cos(ang), cy + R * Math.sin(ang), z + ((zb - z) * k) / steps]);
}
}
return out;
}
Splines as vertices
64-bit
Points along a B-spline, rational or not, sampled evenly in its parameter.
De Boor's algorithm in homogeneous coordinates, then one division by the weight, which is not guarded against zero. Samples are even in the parameter, not in length or turn, so a tightly curved span gets no more points than a straight one.
cad-linework.js
lines 145 to 175
CivilCraft 0.8.1
/** A B-spline (optionally rational) by de Boor, sampled evenly in its parameter: ~32 vertices per control span. */
export function splinePts(sp) {
const P = sp.control || [], p = sp.degree | 0, U = sp.knots || [];
if (P.length > p && p >= 1 && U.length === P.length + p + 1) {
const W = sp.weights && sp.weights.length === P.length ? sp.weights : null;
const u0 = U[p], u1 = U[P.length];
const n = Math.min(4000, Math.max(16, (P.length - p) * 32));
const out = [];
for (let i = 0; i <= n; i++) {
const u = i === n ? u1 : u0 + ((u1 - u0) * i) / n;
let k = p;
while (k < P.length - 1 && u >= U[k + 1]) k++;
const d = [];
for (let j = 0; j <= p; j++) { const q = P[k - p + j], w = W ? W[k - p + j] : 1; d.push([q.x * w, q.y * w, (q.z || 0) * w, w]); }
for (let r = 1; r <= p; r++) {
for (let j = p; j >= r; j--) {
const i0 = k - p + j, den = U[i0 + p - r + 1] - U[i0];
const a = den ? (u - U[i0]) / den : 0;
for (let c = 0; c < 4; c++) d[j][c] = (1 - a) * d[j - 1][c] + a * d[j][c];
}
}
const q = d[p];
out.push([q[0] / q[3], q[1] / q[3], q[2] / q[3]]);
}
return out;
}
const F = (sp.fit || []).map((q) => [q.x, q.y, q.z || 0]);
if (F.length === 2) return F;
if (F.length > 2) return throughFit(F);
return P.length >= 2 ? P.map((q) => [q.x, q.y, q.z || 0]) : [];
}
Splines stored as fit points only
64-bit
A smooth curve through the fit points when the file gives no control points.
centripetal Catmull-Rom: knot step = √(distance between points); 8 vertices per span
The curve passes through every fit point, but between them it is a Catmull-Rom curve, not the spline the source program would draw from the same fit points. A repeated fit point takes a knot step of 1e-9 to avoid a division by zero.
cad-linework.js
lines 177 to 195
CivilCraft 0.8.1
/** A smooth curve through fit points (centripetal Catmull–Rom, 8 vertices a span) — for a spline stored only as fit
* points (DXF writers may leave out the control points). It passes every fit point, as the spline does. */
function throughFit(F) {
const out = [F[0]];
const P = [F[0].map((v, i) => 2 * v - F[1][i]), ...F, F.at(-1).map((v, i) => 2 * v - F.at(-2)[i])];
const tj = (a, b) => Math.sqrt(Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2])) || 1e-9;
for (let i = 1; i < P.length - 2; i++) {
const [p0, p1, p2, p3] = [P[i - 1], P[i], P[i + 1], P[i + 2]];
const t1 = tj(p0, p1), t2 = t1 + tj(p1, p2), t3 = t2 + tj(p2, p3);
for (let k = 1; k <= 8; k++) {
const u = t1 + ((t2 - t1) * k) / 8;
const L = (a, b, ta, tb) => a.map((v, j) => ((tb - u) * v + (u - ta) * b[j]) / (tb - ta));
const A1 = L(p0, p1, 0, t1), A2 = L(p1, p2, t1, t2), A3 = L(p2, p3, t2, t3);
const B1 = L(A1, A2, 0, t2), B2 = L(A2, A3, t1, t3);
out.push(L(B1, B2, t1, t2));
}
}
return out;
}
Ellipses as vertices
64-bit
Points along an ellipse or elliptical arc.
minor = ratio × (N × major); p = c + major·cos s + minor·sin s
Sampled every 5° of the ellipse parameter, at least 4 pieces. The parameter is not the turn angle, so on a flat ellipse the vertices are further apart in direction near the ends of the major axis.
cad-linework.js
lines 205 to 216
CivilCraft 0.8.1
case 'ELLIPSE': {
const c = e.center, M = e.majorAxis, r = e.ratio;
const N = ocs(e.extrusion) ? [e.extrusion.x, e.extrusion.y, e.extrusion.z] : [0, 0, 1];
const L = Math.hypot(...N), n = N.map((v) => v / L);
const mn = [n[1] * M.z - n[2] * M.y, n[2] * M.x - n[0] * M.z, n[0] * M.y - n[1] * M.x].map((v) => v * r); // minor axis
let s0 = e.startParam ?? 0, s1 = e.endParam ?? 2 * Math.PI;
while (s1 <= s0) s1 += 2 * Math.PI;
const full = Math.abs(s1 - s0 - 2 * Math.PI) < 1e-9;
const k = Math.max(4, Math.ceil((s1 - s0) / STEP)), out = [];
for (let i = 0; i <= k; i++) { const s = s0 + ((s1 - s0) * i) / k; out.push([c.x + M.x * Math.cos(s) + mn[0] * Math.sin(s), c.y + M.y * Math.cos(s) + mn[1] * Math.sin(s), (c.z || 0) + M.z * Math.cos(s) + mn[2] * Math.sin(s)]); }
if (full) out.pop();
return { parts: [out], closed: full, wcs: true };
One elevation per object, and flat objects
64-bit
The ELEV attribute of an imported object, and whether its vertices keep a z at all.
ELEV is set when every vertex z is within 1e-9 × max(1, |z|) of the first vertex, a tolerance scaled to the elevation. An object whose every |z| is below 1e-12 (absolute) is stored as 2D and its z is dropped.
The vertices of a feature line written earlier by CivilCraft, scaled into project units.
x, y, z × scale
One multiplication per coordinate by the unit scale. Arcs are restored from the bulge values kept in the drawing's extended data, not refitted from the pieces.
cad-linework.js
lines 38 to 50
CivilCraft 0.8.1
/** CivilCraft feature lines written into this drawing (top-level model space only), in project units. */
export function cadFeatureLines(model, { projectUnit = 'ft', scale = null } = {}) {
const ms = (model?.blocks || []).find((b) => String(b.name).toUpperCase() === '*MODEL_SPACE');
const k = Number.isFinite(scale) && scale > 0 ? scale : unitScale(model?.header || {}, projectUnit).scale;
const out = [];
for (const e of ms?.entities || []) {
if (!isFeatureLine(e) || e.invisible) continue;
const vs = (e.vertices || []).filter((v) => !(v.flags & 16)).map((v) => ({ x: v.x * k, y: v.y * k, z: (v.z || 0) * k }));
const f = featureFromCad(vs, !!e.closed, e.xdata, projectUnit);
if (f) out.push(f);
}
return out;
}
The world position of each block reference, and of each cell of an arrayed reference, in project units.
world = OCS · insert; array cell offset rotated by the reference's rotation; × unit scale
The insertion point is rotated by the extrusion's coordinate system and then multiplied once by the unit scale. Array offsets are column and row index times the spacing, not running sums.
block-points.js
lines 43 to 55
CivilCraft 0.8.1
const ins = e.insert || { x: 0, y: 0, z: 0 };
// the insertion point is in the reference's object coordinate system (its extrusion), as every 2D entity
const O = ocs(e.extrusion) || [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]; // row-major 3 × 4; null = +Z
const ix = ins.x || 0, iy = ins.y || 0, iz = ins.z || 0;
const w = [O[0] * ix + O[1] * iy + O[2] * iz, O[4] * ix + O[5] * iy + O[6] * iz, O[8] * ix + O[9] * iy + O[10] * iz];
const attrs = {};
for (const a of e.attribs || []) { const tag = str(a.tag, 255).toUpperCase(); if (tag && !(tag in attrs)) attrs[tag] = str(a.value ?? a.text, 500); }
const cols = e.type === 'MINSERT' ? Math.max(1, Math.min(1000, e.cols | 0 || 1)) : 1, rows = e.type === 'MINSERT' ? Math.max(1, Math.min(1000, e.rows | 0 || 1)) : 1;
const r = e.rotation || 0, c = Math.cos(r), s = Math.sin(r);
for (let ci = 0; ci < cols; ci++) for (let ri = 0; ri < rows; ri++) {
const dx = ci * (e.colSpacing || 0), dy = ri * (e.rowSpacing || 0);
const ox = c * dx - s * dy, oy = s * dx + c * dy; // MINSERT steps run along the reference's own turned axes (OCS)
const p = [w[0] + O[0] * ox + O[1] * oy, w[1] + O[4] * ox + O[5] * oy, w[2] + O[8] * ox + O[9] * oy].map((v) => v * k);
Number from attribute text
64-bit
The elevation or number read from a block attribute such as EL 105.32.
Takes the first number in the text and reads a comma between digits as a decimal point, so 105,32 reads as 105.32 and 1,234.5 reads as 1.234. The text is parsed as written; nothing is rounded.
block-points.js
lines 78 to 84
CivilCraft 0.8.1
/** A number read from attribute text: "105.32", "105,32", "EL 105.32", "105.32'" — the first number in it. */
export function attrNumber(text) {
const m = /[-+]?\d+(?:[.,]\d+)?(?:e[-+]?\d+)?/i.exec(String(text ?? ''));
if (!m) return null;
const v = Number(m[0].replace(',', '.'));
return Number.isFinite(v) ? v : null;
}
Elevation kept for a block point
64-bit
Which elevation a point made from a block gets: the insertion z, an attribute, or none.
The value is copied, not rounded. An elevation of 1,000,000 or more in absolute value is discarded and the point is stored without one.
block-points.js
lines 104 to 109
CivilCraft 0.8.1
let zz = null;
if (z === 'insert') zz = r.z;
else if (zTag) { zz = attrNumber(r.attrs[zTag]); if (zz === null) skipped.noZ++; }
let d = desc === 'layer' ? r.layer : dTag ? str(r.attrs[dTag]) : r.block;
if (!d) d = str(r.block);
points.push({ n, x: r.x, y: r.y, z: Number.isFinite(zz) && Math.abs(zz) < 1e6 ? zz : null, d: str(d), blk: { name: str(r.block, 255), layer: str(r.layer, 255), handle: str(r.handle, 40) } });
In detail
Method
Unit scale from $INSUNITS. Object coordinate systems by the arbitrary axis algorithm (the code cites the DXF reference). Block references by composing 3×4 affine matrices. Bulge arcs from sweep = 4·atan(bulge). B-splines by de Boor's algorithm; a spline stored only as fit points becomes a centripetal Catmull-Rom curve through them.
Number types
64-bit numbers in plain arrays.
Tolerances
Curve vertices every 5° at most. One elevation per object when every z is within 1e-9 × max(1, |z|) of the first. An object is treated as flat when every |z| is below 1e-12 (absolute). Extrusion taken as +Z when |x| and |y| are below 1e-12. Full ellipse when the parameter span is within 1e-9 of 2π.
Limits
A chord's middle lies up to r × (1 − cos 2.5°), about 0.00095 r, inside an arc of radius r. Feet and US survey feet are never scaled into each other. Block nesting stops at 8 levels and the import at 5,000,000 vertices.
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.