Resources Numerical accuracy Calculation policy

DXF reading: objects from the file's text

Reads the numbers of an ASCII DXF file into drawing objects: coordinates, angles, hatch boundaries and pattern lines, block arrays, dimensions, viewports, lineweights and sheet sizes.

How the precision is kept

Every real number is read with parseFloat, which gives the nearest 64-bit number to the decimal text, so the file's own digits set the accuracy. Angles are written in degrees in DXF and are converted once with × π ÷ 180. Arc edges of a hatch boundary are sampled at 32 chords per full turn and bulge arcs at 16 per half turn, so hatch outlines become polygons.

Open item. Guard the bulge arc of a hatch boundary against a zero-length chord, as the DWG reader does, so two coincident vertices cannot produce points that are not numbers.

Calculations under this policy

15 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.

dxf.js CadCraft 0.8.2 64-bit 15 calculations Reviewed

Tests bin/test-cadcraft.mjs, bin/test-cadcraft-props.mjs

A number from a group value

64-bit

Reads one real number from the file's text, or a default when the group is absent.

value = parseFloat(text)

parseFloat returns the nearest 64-bit number to the decimal text, so a file written with 17 significant digits reads back exactly and one written with fewer reads back to those digits. Text that is not a number gives NaN here; it is not replaced by 0.

  • dxf.js
  • line 571
  • CadCraft 0.8.2
const num = (g, code, d = 0) => { const v = val(g, code); return v === undefined ? d : parseFloat(v); };

Arc angles

64-bit

An arc's center, radius and start and end angles.

a = (degrees × π ÷ 180) mod 2π

The file's degrees are multiplied by Math.PI and divided by 180, then reduced into 0 to 2π. The conversion rounds once; an angle such as 90 degrees becomes the 64-bit number nearest π ÷ 2.

  • dxf.js
  • line 703
  • CadCraft 0.8.2
case 'ARC': return { type: 'arc', ...base, cx: num(g, 10), cy: num(g, 20), r: num(g, 40), a0: normAngle(rad(num(g, 50))), a1: normAngle(rad(num(g, 51))) };

Hatch boundary to polygon rings

64-bit

Turns a hatch's boundary paths (polylines with bulges, lines and arcs) into rings of points.

arc edge: steps = max(4, ⌈32 × (a₁ − a₀) ÷ 2π⌉); p = c + r × (cos a, sin a)

Arc edges become equal-angle chords, an inscribed polygon (32 chords for a full turn). A clockwise edge has both angles negated; then the end angle is raised by 2π when it is not above the start. A path with an ellipse or spline edge is skipped and counted in the report.

  • dxf.js
  • lines 918 to 958
  • CadCraft 0.8.2
function hatchLoops(g, rep) {
  // Walk the boundary paths in order (the codes repeat, so position matters).
  let k = g.findIndex(([c]) => c === 91);
  if (k < 0) return [];
  const loops = [];
  const n = parseInt(g[k][1], 10); k++;
  const next = (code) => { while (k < g.length && g[k][0] !== code) k++; return k < g.length ? parseFloat(g[k++][1]) : NaN; };
  for (let p = 0; p < n && k < g.length; p++) {
    const flags = next(92);
    const ring = [];
    if (flags & 2) {
      const hasBulge = next(72) === 1, closed = next(73) === 1, cnt = next(93);
      const vs = [];
      for (let j = 0; j < cnt; j++) { const x = next(10), y = next(20), b = hasBulge ? next(42) : 0; vs.push({ x, y, b }); }
      for (let j = 0; j < vs.length; j++) {
        const a = vs[j], b2 = vs[(j + 1) % vs.length];
        ring.push(a.x, a.y);
        if (Math.abs(a.b) > 1e-12 && (closed || j + 1 < vs.length)) arcInto(ring, a, b2, a.b);
      }
      if (vs.length) ring.push(vs[0].x, vs[0].y);
    } else {
      const edges = next(93);
      let ok = true;
      for (let j = 0; j < edges; j++) {
        const t = next(72);
        if (t === 1) { const x1 = next(10), y1 = next(20), x2 = next(11), y2 = next(21); ring.push(x1, y1, x2, y2); }
        else if (t === 2) {
          const cx = next(10), cy = next(20), r = next(40), s = rad(next(50)), e = rad(next(51)), ccw = next(73) === 1;
          let a0 = s, a1 = e;
          if (!ccw) { a0 = -s; a1 = -e; }
          if (a1 <= a0) a1 += TAU;
          const steps = Math.max(4, Math.ceil(32 * (a1 - a0) / TAU));
          for (let q = 0; q <= steps; q++) { const a = a0 + (a1 - a0) * q / steps; ring.push(cx + r * Math.cos(a), cy + r * Math.sin(a)); }
        } else { ok = false; break; }   // ellipse / spline edges: not yet
      }
      if (!ok) { skip(rep, t('dxf.hatch_boundary_ellipse_spline')); continue; }
    }
    if (ring.length >= 6) loops.push(ring);
  }
  return loops;
}

Bulge arc to points

64-bit

The points along a bulged segment of a hatch boundary polyline.

θ = 4 × atan(b); r = |c ÷ (2 × sin(θ ÷ 2))|; center = midpoint + normal × r × cos(θ ÷ 2); steps = max(2, ⌈16 × |θ| ÷ π⌉)

Closed form from the chord length c and the bulge. The direction is divided by c with no check: two coincident vertices with a bulge give 0 ÷ 0 and the points added are NaN. The reader of DWG files returns early in that case; this one does not.

  • dxf.js
  • lines 960 to 967
  • CadCraft 0.8.2
function arcInto(ring, p, q, b) {
  const theta = 4 * Math.atan(b), steps = Math.max(2, Math.ceil(16 * Math.abs(theta) / Math.PI));
  const c = Math.hypot(q.x - p.x, q.y - p.y), r = Math.abs(c / (2 * Math.sin(theta / 2)));
  const mx = (p.x + q.x) / 2, my = (p.y + q.y) / 2, h = r * Math.cos(theta / 2) * (b > 0 ? 1 : -1);
  const ux = (q.x - p.x) / c, uy = (q.y - p.y) / c, cx = mx - uy * h, cy = my + ux * h;
  const a0 = Math.atan2(p.y - cy, p.x - cx);
  for (let s = 1; s < steps; s++) { const a = a0 + theta * s / steps; ring.push(cx + r * Math.cos(a), cy + r * Math.sin(a)); }
}

Hatch pattern scale, angle and lines

64-bit

A pattern hatch's scale, angle and its own pattern lines.

pattern angle = (degrees × π ÷ 180) mod 2π; dx = o_x cos a + o_y sin a; dy = −o_x sin a + o_y cos a, a = line angle × π ÷ 180

Each line's angle stays in degrees as read; its offset is rotated from world axes into the line's frame. A hatch with more than 64 line families keeps none of them, each family keeps at most 32 dashes, and a family with any value that is not a finite number is left out.

  • dxf.js
  • lines 517 to 545
  • CadCraft 0.8.2
/**
 * A pattern HATCH's scale (41), angle (52, degrees → radians) and its own pattern lines after group 78 (53 angle, 43/44
 * base, 45/46 offset in world axes, 79 dash count, 49 dashes) → { pscale, pangle, patlines } in hatch.js's shape
 * (angle in degrees, offset along / across the line), so hatch.js draws the file's lines as they are.
 */
function hatchPattern(g) {
  const out = {};
  const sc = num(g, 41, 1), an = num(g, 52, 0);
  if (sc > 0 && sc !== 1) out.pscale = sc;
  if (an) out.pangle = normAngle(rad(an));
  const k = g.findIndex(([c]) => c === 78);
  if (k < 0) return out;
  const lines = [];
  let cur = null;
  for (let j = k + 1; j < g.length; j++) {
    const [c, v] = g[j], x = parseFloat(v);
    if (c === 53) { cur = { angle: x, x: 0, y: 0, ox: 0, oy: 0, dashes: [] }; lines.push(cur); }
    else if (!cur) break;
    else if (c === 43) cur.x = x; else if (c === 44) cur.y = x; else if (c === 45) cur.ox = x; else if (c === 46) cur.oy = x;
    else if (c === 49) cur.dashes.push(x);
    else if (c !== 79) break;
  }
  const ok = lines.filter((l) => [l.angle, l.x, l.y, l.ox, l.oy, ...l.dashes].every(Number.isFinite));
  if (ok.length && ok.length <= 64) out.patlines = ok.map((l) => {
    const a = l.angle * Math.PI / 180, ca = Math.cos(a), sa = Math.sin(a);
    return { angle: l.angle, x: l.x, y: l.y, dx: l.ox * ca + l.oy * sa, dy: -l.ox * sa + l.oy * ca, dashes: l.dashes.slice(0, 32) };
  });
  return out;
}

Block array positions

64-bit

The insertion point of each copy of a block placed as a rows-by-columns array.

p = T(insert) × R(rot) × (c × column spacing, r × row spacing)

Each offset is index × spacing, not a running sum, and goes through one translate-rotate matrix built once.

  • dxf.js
  • lines 797 to 802
  • CadCraft 0.8.2
const cols = Math.max(1, parseInt(val(g, 70) ?? '1', 10)), rows = Math.max(1, parseInt(val(g, 71) ?? '1', 10));
const out = [];
if (cols * rows > 1) {
  const dc = num(g, 44), dr = num(g, 45), m = mMul(mTranslate(ins.x, ins.y), mRotate(ins.rot));
  for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) { const p = mApply(m, { x: c * dc, y: r * dr }); out.push({ ...ins, x: p.x, y: p.y }); }
} else out.push(ins);

Dimension defining points

64-bit

The points a dimension measures from, by dimension type.

linear: p₁ = 13, p₂ = 14, line point = 10, angle = group 50 × π ÷ 180; diameter center = (point 15 + point 10) ÷ 2; four-point angular: vertex from the two lines

The points are read as written and the dimension measures again from them. The measurement stored in the file (group 42) is not read here, so a file whose points and stored value disagree shows the value of its points.

  • dxf.js
  • lines 827 to 835
  • CadCraft 0.8.2
if (kind === 0 || kind === 1) d = { kind: kind ? 'aligned' : 'linear', p1: pt(13), p2: pt(14), loc: pt(10), rot: normAngle(rad(num(g, 50))) };
else if (kind === 4) d = { kind: 'radius', c: pt(10), p1: pt(15), loc: pt(11) };
else if (kind === 3) { const a = pt(15), b = pt(10); d = { kind: 'diameter', c: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, p1: a, loc: pt(11) }; }
else if (kind === 5) d = { kind: 'angular', c: pt(15), p1: pt(13), p2: pt(14), loc: pt(10) };
else if (kind === 2) {
  const L = (a, b) => ({ x1: num(g, a), y1: num(g, a + 10), x2: num(g, b), y2: num(g, b + 10) });
  const r = angularFromLines(L(13, 14), L(15, 10), pt(16));
  if (r) d = { kind: 'angular', ...r, loc: pt(16) };
}

Can the dimension be measured

64-bit

Checks that a dimension's points are finite and far enough apart to measure.

|a − b| > 1e-12

Absolute 1e-12 drawing units between the points that span the measurement. A dimension that fails is drawn from the file's block, or skipped when it has none.

  • dxf.js
  • lines 881 to 889
  • CadCraft 0.8.2
/** Can CadCraft draw this dimension itself (finite points, something to measure)? */
function dimUsable(d) {
  const pts = ['p1', 'p2', 'c', 'loc'].filter((k) => d[k]).map((k) => d[k]);
  if (!pts.every((p) => Number.isFinite(p.x) && Number.isFinite(p.y))) return false;
  const far = (a, b) => Math.hypot(b.x - a.x, b.y - a.y) > 1e-12;
  if (d.kind === 'linear' || d.kind === 'aligned') return far(d.p1, d.p2) && Number.isFinite(d.rot ?? 0);
  if (d.kind === 'radius' || d.kind === 'diameter') return far(d.c, d.p1);
  return far(d.c, d.p1) && far(d.c, d.p2) && far(d.c, d.loc);
}

Viewport scale, center and twist

64-bit

A sheet viewport's scale, the model point at its center and its view twist.

s = viewport height ÷ view height; twist = degrees × π ÷ 180 (or radians from R12 data)

One division for the scale; without a positive view height the scale is 1 and the center is the viewport's own. R12 files carry the view in extended data, where the twist is already in radians. A twist below 1e-12 radians (absolute) is dropped.

  • dxf.js
  • lines 855 to 871
  • CadCraft 0.8.2
const tw0 = val(g, 51) !== undefined ? rad(num(g, 51)) : (xlist(xd.ACAD, 'MVIEW') || []).filter(([c]) => c === 1040).map(([, x]) => +x)[0];
const twc = Number.isFinite(tw0) ? tw0 : 0;
if (val(g, 45) !== undefined) { vh = num(g, 45); vc = vpModelCentre({ x: num(g, 12), y: num(g, 22) }, { x: num(g, 17), y: num(g, 27) }, twc); }
else {
  const mv = xlist(xd.ACAD, 'MVIEW');   // R12: 1070 16, target (1010…), direction, then 1040 twist, height, centre x, centre y, …
  if (mv) {
    const reals = mv.filter(([c]) => c === 1040).map(([, x]) => +x);
    const tgt = mv.find(([c]) => c === 1010), tgy = mv.find(([c]) => c === 1020);
    vh = reals[1]; vc = vpModelCentre({ x: reals[2], y: reals[3] }, { x: tgt ? +tgt[1] : 0, y: tgy ? +tgy[1] : 0 }, twc);
    const frz = mv.filter(([c]) => c === 1003).map(([, n]) => n).filter(Boolean);   // R12: the frozen layers' names (VP.1)
    if (frz.length) v.frz = [...new Set(frz)];
  }
}
const fh = g.filter(([c]) => c === 331).map(([, x]) => x.trim()).filter(Boolean);   // DXF 2000+: frozen layers' handles, named in buildLayouts
if (fh.length) v._frzH = fh;
if (vc && vh > 0 && v.h > 0) { v.cx = vc.x; v.cy = vc.y; v.s = v.h / vh; } else { v.cx = v.x; v.cy = v.y; v.s = 1; }
if (Math.abs(twc) > 1e-12) v.tw = twc;   // its twist (DR.8)

Layer lineweight

64-bit

A layer's lineweight in millimeters.

mm = n ÷ 100 for 0 ≤ n ≤ 211; otherwise default

The file holds a whole number of hundredths of a millimeter, read with parseInt. Values outside 0 to 211 become the default code.

  • dxf.js
  • lines 199 to 202
  • CadCraft 0.8.2
const lw = parseInt(val(g, 370) ?? '-3', 10);
const lh = val(g, 5); if (lh) layerByH.set(lh.toUpperCase(), name);
out.layers.push({ name, color: tc !== undefined ? trueColor(tc) : (Math.abs(col) || 7), on: col >= 0, frozen: !!(flags & 1), locked: !!(flags & 4), ...(flags & 2 ? { newVpFrozen: true } : {}), plot: val(g, 290) !== '0',
  linetype: val(g, 6) || 'Continuous', lineweight: lw >= 0 && lw <= 211 ? lw / 100 : -3 });

Object lineweight and linetype scale

64-bit

An object's own lineweight in millimeters and its linetype scale.

lw = n ÷ 100; linetype scale kept when |s − 1| > 1e-12

The lineweight is a whole number of hundredths divided by 100. A linetype scale within 1e-12 of 1 is treated as 1 and not stored.

  • dxf.js
  • lines 586 to 596
  • CadCraft 0.8.2
/** An object's own linetype (6), lineweight (370, 1/100 mm) and linetype scale (48) — absent = by layer (0.4 C8). */
function styleOf(g) {
  const o = {};
  const lt = val(g, 6);
  if (lt && !/^bylayer$/i.test(lt)) o.lt = /^byblock$/i.test(lt) ? 'ByBlock' : /^continuous$/i.test(lt) ? 'Continuous' : lt;
  const lw = val(g, 370);
  if (lw !== undefined) { const n = parseInt(lw, 10); if (n === -2 || n === -3) o.lw = n; else if (n >= 0 && n <= 211) o.lw = n / 100; }
  const s = parseFloat(val(g, 48));
  if (s > 0 && Math.abs(s - 1) > 1e-12) o.lts = s;
  return o;
}

Sheet size from the plot settings

64-bit

A sheet's size in its own unit when the layout gives no limits, from the paper size in millimeters.

size = paper mm ÷ 25.4 (inch sheets) or ÷ 1; width and height swapped when the plot rotation code is odd

25.4 is exact; the division rounds once. The layout's limits are used unchanged when they span a positive width and height.

  • dxf.js
  • lines 457 to 458
  • CadCraft 0.8.2
let lim = l.max.x > l.min.x && l.max.y > l.min.y ? l : null;
if (!lim && l.paperMm.w > 0) { const k = unit === 'in' ? 25.4 : 1, [w, h] = l.rot % 2 ? [l.paperMm.h, l.paperMm.w] : [l.paperMm.w, l.paperMm.h]; lim = { min: { x: 0, y: 0 }, max: { x: w / k, y: h / k } }; }

Dimension style numbers

64-bit

Brings a dimension style's scale, unit format and decimal places into range.

scale ≤ 0 → 1; dec, adec, tad, zin → max(0, round(value)); dec ≤ 8

The number of decimal places is rounded to a whole number and capped at 8. Sizes are left as the 64-bit numbers read.

  • dxf.js
  • lines 507 to 515
  • CadCraft 0.8.2
function fixStyle(st) {
  if (!(st.scale > 0)) st.scale = 1;
  // DIMLUNIT (277, R2000+) is units.js' code; R12's DIMUNIT (270) adds 6 / 7 for the unstacked architectural / fractional
  if (st.unit !== undefined) { const u = Math.round(st.unit); if (st.lunit === undefined) st.lunit = u === 6 ? 4 : u === 7 ? 5 : u; delete st.unit; }
  if (st.lunit !== undefined) { const u = Math.round(+st.lunit); if (u >= 1 && u <= 5) st.lunit = u; else if (u === 6) st.lunit = 2; else delete st.lunit; }
  for (const k of ['dec', 'adec', 'tad', 'zin']) st[k] = Math.max(0, Math.round(st[k] || 0));
  if (st.dec > 8) st.dec = 8;
  return st;
}

Construction line direction

64-bit

The unit direction of an infinite line or ray.

direction = (dx, dy) ÷ √(dx² + dy²)

Normalized again with Math.hypot. A direction not longer than 1e-12 (absolute) is skipped and counted.

  • dxf.js
  • lines 696 to 698
  • CadCraft 0.8.2
const dx = num(g, 11), dy = num(g, 21), n = Math.hypot(dx, dy);
if (!(n > 1e-12)) { skip(rep, type); return null; }
return { type: type === 'XLINE' ? 'xline' : 'ray', ...base, x: num(g, 10), y: num(g, 20), dx: dx / n, dy: dy / n };

Extra sheet read back from a block

64-bit

Rebuilds a sheet and its viewports from the block an R12 file keeps them in.

viewport center = ((x_min + x_max) ÷ 2, (y_min + y_max) ÷ 2); w = x_max − x_min; h = y_max − y_min

A viewport's rectangle is recovered from the corners of its frame polyline, so its center and size are a mean and a difference of written coordinates. Its view center and scale are read from numbers this app wrote as text with 15 significant digits.

  • dxf.js
  • lines 488 to 505
  • CadCraft 0.8.2
/** A CCLAYOUT_n block (CadCraft's extra layouts in R12) → a layout, or null when it is not one. */
function layoutFromBlock(ents) {
  const mark = ents.find((e) => e.cc && e.cc[0] === 'LAYOUT');
  if (!mark) return null;
  const [, name, unit, key, x0, y0, w, h] = mark.cc;
  return {
    name: decodeText(String(name || 'Layout')), paper: { unit: unit === 'in' ? 'in' : 'mm', key: key || undefined, x0: +x0 || 0, y0: +y0 || 0, w: +w, h: +h },
    entities: ents.filter((e) => e !== mark).map((e) => {
      if (e.cc && e.cc[0] === 'VIEWPORT' && e.type === 'poly' && e.pts.length >= 4) {
        const xs = e.pts.map((p) => p.x), ys = e.pts.map((p) => p.y);
        const x1 = Math.min(...xs), x2 = Math.max(...xs), y1 = Math.min(...ys), y2 = Math.max(...ys);
        const frz = e.cc[4] === 'FRZ' ? e.cc.slice(5).filter(Boolean) : [];   // layers frozen in it (VP.1)
        return { type: 'viewport', layer: e.layer, color: e.color, x: (x1 + x2) / 2, y: (y1 + y2) / 2, w: x2 - x1, h: y2 - y1, cx: +e.cc[1], cy: +e.cc[2], s: +e.cc[3], ...(frz.length ? { frz } : {}) };
      }
      const c = { ...e }; delete c.cc; return c;
    }),
  };
}

In detail

Method
parseFloat of each group value; degrees to radians; closed-form arc from a bulge (θ = 4 atan b); block arrays through one translate-rotate matrix.
Number types
64-bit numbers. Lineweights are whole hundredths of a millimeter in the file and are divided by 100.
Tolerances
Zero lengths 1e-12 (absolute, drawing units); a bulge counts as an arc above 1e-12; a view twist or linetype scale is kept when it differs from 0 or 1 by more than 1e-12.
Limits
Arc edges of hatch boundaries become chords; a boundary with ellipse or spline edges is skipped and counted. Unlike the DWG reader, a dimension's stored measurement is not compared with its points.

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.

Every policy that takes this approach