Resources Numerical accuracy Calculation policy

DXF writing: numbers as text

Writes a drawing's coordinates, sizes and angles as decimal text in a DXF file, in the R12 and 2000 formats.

How the precision is kept

Every real number goes through one formatter that rounds to 15 significant digits and writes a magnitude below 1e-12 as 0.0. A 64-bit number can need 17 significant digits to read back unchanged, so a value saved to DXF can read back a few units in its last place away from the value in the drawing. Angles are converted to degrees before they are written. R12 has no ellipse, spline or hatch object, so those are written as polylines.

Open item. Write real numbers with 17 significant digits, or the shortest text that reads back to the same 64-bit value, and drop the absolute 1e-12 cut.

Calculations under this policy

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

dxf.js CadCraft 0.8.2 64-bit 13 calculations Reviewed

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

Number text (R12)

64-bit

The text every real number is written as in an R12 file.

|n| < 1e-12 → 0.0; otherwise n rounded to 15 significant digits

toPrecision(15) rounds the value before it is written, so the saved number can differ from the drawing's by up to 5 units in the 16th significant digit. A magnitude below 1e-12 is written as 0.0 whatever the drawing's scale. There is no check for a number that is not finite: NaN or Infinity would be written as those words.

  • dxf.js
  • line 1016
  • CadCraft 0.8.2
const f = (n) => (Math.abs(n) < 1e-12 ? '0.0' : Number(n.toPrecision(15)).toString());

Point (R12)

64-bit

Writes a 2D point as three groups.

x → f(x); y → f(y); z → 0.0

x and y go through the 15-digit formatter. z is always written as 0.0 by this helper; objects with a height write their z through a separate path.

  • dxf.js
  • line 1017
  • CadCraft 0.8.2
const xy = (c, p) => { w(c, f(p.x)); w(c + 10, f(p.y)); w(c + 20, '0.0'); };

Arc (R12)

64-bit

An arc's center, radius and its angles in degrees.

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

The angle is converted to degrees and then cut to 15 significant digits; the reader multiplies back by π ÷ 180. An arc angle therefore goes through two rounded conversions and one decimal rounding on a save and open.

  • dxf.js
  • line 1472
  • CadCraft 0.8.2
case 'arc': head('ARC', e, o); w(10, f(e.cx)); w(20, f(e.cy)); w(30, zf(e.z)); w(40, f(e.r)); w(50, f(deg(normAngle(e.a0)))); w(51, f(deg(normAngle(e.a1)))); grow(e.cx - e.r, e.cy - e.r, o); grow(e.cx + e.r, e.cy + e.r, o); xd(e, o); break;

Dimension (R12)

64-bit

The defining points, text point and angles written for a dimension.

linear: point 10 = end of the dimension line on the second extension line; radial: near = c + r × (cos a, sin a), far = c − r × (cos a, sin a); angles in degrees

Points are recomputed from the dimension's defining points in 64-bit and written with 15 digits. The measured value itself is not written; a reader measures again from the points. The text rotation is written as full-precision number text in the app's own extended data.

  • dxf.js
  • lines 1413 to 1459
  • CadCraft 0.8.2
function dimension(e, o) {
  const name = dimName.get(e);
  if (!name) return;
  head('DIMENSION', e, o);
  w(2, name);
  const tp = dimTextPoint(e);
  const ds = { ...DIM_DEFAULTS, ...(e.st || {}) };
  switch (e.kind) {
    case 'linear': case 'aligned': {
      const r = e.kind === 'aligned' ? Math.atan2(e.p2.y - e.p1.y, e.p2.x - e.p1.x) : (e.rot || 0);
      const u = { x: Math.cos(r), y: Math.sin(r) }, d2 = (e.p2.x - e.loc.x) * u.x + (e.p2.y - e.loc.y) * u.y;
      const { q2 } = dimLinePoints(e);
      xy(10, q2); xy(11, tp); w(70, e.kind === 'aligned' ? 1 : 0);
      if (e.text) w(1, encodeText(e.text));
      xy(13, e.p1); xy(14, e.p2);
      if (e.kind === 'linear') w(50, f(deg(normAngle(e.rot || 0))));
      if (Number.isFinite(e.obl) && e.obl) w(52, f(deg(normAngle(e.obl))));   // slanted extension lines (0.7 D7.14)
      break;
    }
    case 'radius': case 'diameter': {
      const r = Math.hypot(e.p1.x - e.c.x, e.p1.y - e.c.y);
      const dl = Math.hypot(e.loc.x - e.c.x, e.loc.y - e.c.y);
      const a = dl > 1e-12 ? Math.atan2(e.loc.y - e.c.y, e.loc.x - e.c.x) : Math.atan2(e.p1.y - e.c.y, e.p1.x - e.c.x);
      const near = { x: e.c.x + r * Math.cos(a), y: e.c.y + r * Math.sin(a) }, far = { x: e.c.x - r * Math.cos(a), y: e.c.y - r * Math.sin(a) };
      xy(10, e.kind === 'radius' ? e.c : far); xy(11, tp); w(70, e.kind === 'radius' ? 4 : 3);
      if (e.text) w(1, encodeText(e.text));
      xy(15, near); w(40, '0.0');
      break;
    }
    case 'angular':
      xy(10, e.loc); xy(11, tp); w(70, 5);
      if (e.text) w(1, encodeText(e.text));
      xy(13, e.p1); xy(14, e.p2); xy(15, e.c);
      break;
    default: break;
  }
  w(3, e.dimstyle && !/^standard$/i.test(e.dimstyle) ? r12Name(e.dimstyle) : 'STANDARD');
  // this dimension's own settings, as AutoCAD stores per-dimension overrides: every variable R12 has (dwg-props.js; its
  // DIMUNIT carries CadCraft's feet and inches and fractions as the unstacked kinds 6, 7)
  w(1001, 'ACAD');
  for (const [code, v] of dstyleItems(ds, { r12: true })) w(code, code === 1040 ? f(+v) : code === 1000 ? encodeText(String(v)) : v);
  // CadCraft's decimals and length format, the text rotation (no R12 field) and what no R12 variable holds (CCDIMST)
  w(1001, 'CADCRAFT'); w(1000, 'DIM'); w(1070, ds.dec | 0); w(1070, ds.adec | 0); w(1070, ds.zin | 0); w(1070, ds.lunit || 2);
  if (e.trot) w(1000, 'CCTROT:' + e.trot);
  for (const [, v] of ccDimItems(ds, { r12: true })) w(1000, encodeText(v));
  note('dims');
}

Viewport view (R12)

64-bit

A sheet viewport's twist, view height and view center.

view height = h ÷ s

One division of the viewport's height by its scale, written with 15 digits. The division is not guarded against a scale of 0, and the formatter does not check for a result that is not finite.

  • dxf.js
  • lines 1331 to 1332
  • CadCraft 0.8.2
const tw = Number.isFinite(e.tw) ? e.tw : 0, dc = vpDcsCentre(e.cx, e.cy, tw);   // the centre in display coordinates (CAD-TWIST-SAVE)
w(1040, f(tw)); w(1040, f(e.h / e.s)); w(1040, f(dc.x)); w(1040, f(dc.y));   // twist (radians, DR.8), height, centre

Ellipses and splines as polylines (R12)

64-bit

Writes an ellipse or a spline as a polyline along the curve, because R12 has neither object.

The points come from ellipsePoints in geom.js and splinePolyline(e, 10) in spline.js; the saved object is a polygon through those points and no longer the curve. Each such object is counted and named in the save report.

  • dxf.js
  • lines 1474 to 1475
  • CadCraft 0.8.2
case 'ellipse': note('ellipse'); polyline(e, ellipsePoints(e).map((p) => ({ ...p, b: 0 })), e.t1 - e.t0 >= TAU - 1e-9, o); break;
case 'spline': { if (!splineValid(e)) break; note('spline'); const pts = splinePolyline(e, 10).map((p) => ({ ...p, b: 0 })); if (pts.length >= 2) polyline(e, pts, !!e.closed, o); break; }   // R12 has no SPLINE: a polyline along the curve

Hatches as solids or outlines (R12)

64-bit

Writes a filled triangle or quadrilateral as a SOLID and any other hatch as closed polylines of its rings.

Ring coordinates are written as they are, each through the 15-digit formatter. A hatch that is not a 3 or 4 cornered solid loses its fill and pattern in R12 and is counted in the report.

  • dxf.js
  • lines 1478 to 1488
  • CadCraft 0.8.2
const rings = e.loops.map((l) => { const pts = []; for (let k = 0; k + 1 < l.length; k += 2) pts.push({ x: l[k], y: l[k + 1], b: 0 }); return pts; });
// A filled triangle or quadrilateral (arrowheads, imported SOLIDs) is an R12 SOLID; anything else keeps its outline.
const q = rings.length === 1 && e.solid ? openRing(rings[0]) : null;
if (q && q.length >= 3 && q.length <= 4) {
  head('SOLID', e, o);
  const [a, b, c, d = c] = q;
  xy(10, a); xy(11, b); xy(12, d); xy(13, c);   // SOLID corners run 1, 2, 4, 3
  break;
}
note('hatch');
for (const pts of rings) polyline(e, pts, true, o);

Text (R12)

64-bit

A text's position, height, rotation, width factor and oblique angle.

rotation° = rot × 180 ÷ π; oblique° = obl × 180 ÷ π

Rotation and oblique angle are converted to degrees and written with 15 digits. The anchor point is written twice (groups 10 and 11) for aligned text, with the same value.

  • dxf.js
  • lines 1377 to 1390
  • CadCraft 0.8.2
function textOut(e, s, x, y, o) {
  head('TEXT', e, o);
  w(10, f(x)); w(20, f(y)); w(30, zf(e.z)); w(40, f(e.h)); w(1, encodeText(s));
  if (e.rot) w(50, f(deg(e.rot)));
  if (e.wf && e.wf !== 1) w(41, f(e.wf));
  if (e.obl) w(51, f(deg(e.obl)));   // oblique angle (P.7)
  if (e.style && !/^standard$/i.test(e.style)) w(7, r12Name(e.style));
  const h72 = e.ha === 'center' ? 1 : e.ha === 'right' ? 2 : 0;
  const v73 = e.va === 'middle' ? 2 : e.va === 'top' ? 3 : e.va === 'bottom' ? 1 : 0;
  if (h72) w(72, h72);
  if (h72 || v73) { w(11, f(x)); w(21, f(y)); w(31, zf(e.z)); }
  if (v73) w(73, v73);
  xd(e, o);
}

Multi-line text as single lines (R12)

64-bit

The position of each line of a multi-line text, written as separate single-line texts.

p = T(x, y) × R(rot) × (anchor x of the text box, baseline of line k)

Line positions come from textLayout in geom.js and one translate-rotate matrix. The lines are placed from the app's own font metrics, so another program's font can set them differently.

  • dxf.js
  • lines 1496 to 1498
  • CadCraft 0.8.2
const m = mMul(mTranslate(e.x, e.y), mRotate(e.rot || 0));
const ax = e.ha === 'center' ? lay.box.x + lay.box.w / 2 : e.ha === 'right' ? lay.box.x + lay.box.w : lay.box.x;
lay.lines.forEach((s, k) => { const p = mApply(m, { x: ax, y: lay.baselines[k] }); textOut({ ...e, va: 'baseline' }, s, p.x, p.y, o); grow(p.x, p.y, o); });

Construction lines as segments (R12)

64-bit

Writes an infinite line or ray as a line across the drawing's extents.

a = p + d × t₀; b = p + d × t₁

The two parameters come from clipping against the drawing's bounding box (clipInfinite in geom.js). The saved segment keeps the line's position and direction but not that it was infinite; each one is counted in the report.

  • dxf.js
  • lines 1463 to 1466
  • CadCraft 0.8.2
const tt = clipInfinite({ x: e.x, y: e.y }, { x: e.dx, y: e.dy }, drawingExtents(), e.type === 'ray');
note('infinite');
if (!tt || !(tt[1] > tt[0])) break;
const a = { x: e.x + e.dx * tt[0], y: e.y + e.dy * tt[0] }, b = { x: e.x + e.dx * tt[1], y: e.y + e.dy * tt[1] };

Extents of what was written (R12)

64-bit

The smallest and largest x and y of the model-space objects written.

min and max over the written points

Comparisons on the 64-bit values before formatting; the result is returned in the save report. Circles and arcs add their center ± radius, so an arc counts as its whole circle.

  • dxf.js
  • line 1363
  • CadCraft 0.8.2
function grow(x, y, o) { if (!o.top) return; ext.minX = Math.min(ext.minX, x); ext.minY = Math.min(ext.minY, y); ext.maxX = Math.max(ext.maxX, x); ext.maxY = Math.max(ext.maxY, y); }

Linetype pattern (R12)

64-bit

A linetype's total pattern length and its dashes and gaps.

length = Σ |elementᵢ|

The absolute values are added in order and the sum and each element are written with 15 digits.

  • dxf.js
  • lines 1151 to 1152
  • CadCraft 0.8.2
w(0, 'LTYPE'); w(2, n); w(70, 0); w(3, doc.linetypes?.get(n)?.description || n); w(72, 65); w(73, pat.length); w(40, f(pat.reduce((a, v) => a + Math.abs(v), 0)));
for (const v of pat) w(49, f(v));

Center mark size (R12)

64-bit

The center mark size written with each dimension style.

DIMCEN = arrow size ÷ 2 (2.5 when no arrow size is set)

Half the style's arrow size, the same proportion dims.js uses for the center mark of a radius or diameter dimension.

  • dxf.js
  • line 1212
  • CadCraft 0.8.2
w(141, f(((s2.asz ?? st.asz) || 2.5) / 2));
dxf2000.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

Tests bin/test-cadcraft.mjs, bin/test-cad-round3.mjs

Number text (2000)

64-bit

The text every real number is written as in a DXF 2000 file.

not finite → 0.0; |n| < 1e-12 → 0.0; otherwise n rounded to 15 significant digits

The same 15-digit rounding and absolute 1e-12 cut as the R12 writer. Here a number that is not finite is written as 0.0, with no note in the report.

  • dxf2000.js
  • line 22
  • CadCraft 0.8.2
const f = (n) => (Number.isFinite(n) ? (Math.abs(n) < 1e-12 ? '0.0' : Number(n.toPrecision(15)).toString()) : '0.0');

Radians to degrees (2000)

64-bit

Converts an angle to the degrees DXF stores.

degrees = (r × 180) ÷ π

The multiplication by 180 comes first, then the division by Math.PI: two rounded operations before the 15-digit text.

  • dxf2000.js
  • line 21
  • CadCraft 0.8.2
const deg = (r) => (r * 180) / Math.PI;

Point (2000)

64-bit

Writes a point as three groups.

x → f(x); y → f(y); z → f(z); a missing coordinate → 0.0

Each coordinate goes through the 15-digit formatter. A coordinate that is missing or NaN is written as 0.0.

  • dxf2000.js
  • line 219
  • CadCraft 0.8.2
const P = (c, p) => { w(c, f(p?.x || 0)); w(c + 10, f(p?.y || 0)); w(c + 20, f(p?.z || 0)); };

Arc (2000)

64-bit

An arc's center, radius and its angles in degrees.

start° = (a₀ × 180) ÷ π; end° = (a₁ × 180) ÷ π

The radians kept by the drawing are converted to degrees and cut to 15 significant digits. The angles are not reduced modulo 2π here.

  • dxf2000.js
  • line 233
  • CadCraft 0.8.2
case 'ARC': head('ARC', e, owner, paper); w(100, 'AcDbCircle'); th(e); P(10, e.center); w(40, f(e.radius)); w(100, 'AcDbArc'); w(50, f(deg(e.startAngle))); w(51, f(deg(e.endAngle))); break;

Ellipse (2000)

64-bit

An ellipse's center, major axis, axis ratio and start and end parameters, with the ratio brought to 1 or less.

ratio > 1: major′ = (−m_y × ratio, m_x × ratio); ratio′ = 1 ÷ ratio; parameters − π ÷ 2

When the stored ratio is above 1 the major axis is turned a quarter turn and scaled, the ratio inverted and both parameters shifted by π ÷ 2. The reciprocal and the shift each round once. The ellipse stays a true ellipse in this format.

  • dxf2000.js
  • lines 245 to 249
  • CadCraft 0.8.2
case 'ELLIPSE': {
  // DXF wants the ratio ≤ 1: a taller one turns its major axis a quarter turn (as cc-dwg's writer does)
  let maj = e.majorAxis, ratio = e.ratio, s0 = e.startParam ?? 0, s1 = e.endParam ?? 2 * Math.PI;
  if (ratio > 1) { maj = { x: -maj.y * ratio, y: maj.x * ratio, z: 0 }; ratio = 1 / ratio; s0 -= Math.PI / 2; s1 -= Math.PI / 2; }
  head('ELLIPSE', e, owner, paper); w(100, 'AcDbEllipse'); P(10, e.center); P(11, maj); w(40, f(ratio)); w(41, f(s0)); w(42, f(s1));

Hatch pattern lines (2000)

64-bit

A pattern hatch's angle, scale and each pattern line's angle, base, offset and dashes.

angles° = (radians × 180) ÷ π

The line families come from dwg-save.js in radians and world axes and are written with angles in degrees. Every value goes through the 15-digit formatter.

  • dxf2000.js
  • lines 305 to 306
  • CadCraft 0.8.2
w(52, f(deg(e.angle || 0))); w(41, f(e.scale || 1)); w(77, 0); w(78, (e.lines || []).length);
for (const ln of e.lines || []) { w(53, f(deg(ln.angle))); w(43, f(ln.base.x)); w(44, f(ln.base.y)); w(45, f(ln.offset.x)); w(46, f(ln.offset.y)); w(79, ln.dashes.length); for (const d of ln.dashes) w(49, f(d)); }

Spline (2000)

64-bit

A spline's degree, knots, weights, control points and fit points.

knot, control point and fit point tolerances written as 1E-10

Knots, weights and points are written through the 15-digit formatter, so the curve another program rebuilds can differ from the drawing's in the 16th digit of its data. The three tolerance fields are the fixed text 1E-10, not values the app computed.

  • dxf2000.js
  • lines 311 to 322
  • CadCraft 0.8.2
case 'SPLINE': {   // 0.7 D7.10 (spline.js): control points, knots, weights and fit points as DXF keeps them
  const ctrl = e.control || [], fit = e.fit || [], knots = e.knots || [], ws = e.weights;
  head('SPLINE', e, owner, paper); w(100, 'AcDbSpline');
  w(210, '0.0'); w(220, '0.0'); w(230, '1.0');
  w(70, (e.flags | 0) & 15); w(71, e.degree); w(72, knots.length); w(73, ctrl.length); w(74, fit.length);
  w(42, '1E-10'); w(43, '1E-10'); w(44, '1E-10');
  if (fit.length && e.startTangent && (e.startTangent.x || e.startTangent.y)) P(12, e.startTangent);
  if (fit.length && e.endTangent && (e.endTangent.x || e.endTangent.y)) P(13, e.endTangent);
  for (const k of knots) w(40, f(k));
  if (ws) for (const x of ws) w(41, f(x));
  for (const p of ctrl) P(10, p);
  for (const p of fit) P(11, p);

Extended data values (2000)

64-bit

Writes the extra values attached to an object: text, reals and whole numbers.

code 1040: f(value); code 1070: value | 0

Reals (1040) go through the 15-digit formatter. Whole-number items (1070) are passed through a bitwise | 0, which truncates toward zero and wraps at 32 bits; the values this app puts there are group codes and whole-number settings.

  • dxf2000.js
  • lines 213 to 218
  • CadCraft 0.8.2
const xdata = (e) => {
  for (const [app, items] of Object.entries(e.xdata || {})) {
    w(1001, app.toUpperCase());
    for (const [code, v] of items) w(code, code === 1000 ? enc(v) : code === 1040 ? f(+v) : code === 1070 ? (v | 0) : v);
  }
};

In detail

Method
Number(n.toPrecision(15)).toString(); angles × 180 ÷ π; dimension points recomputed from the dimension's defining points.
Number types
64-bit numbers written as decimal text with at most 15 significant digits.
Tolerances
|n| < 1e-12 is written as 0.0 (absolute); a ring's repeated closing point is dropped within 1e-12; an ellipse is closed when its parameter range is within 1e-9 of 2π.
Limits
15 significant digits, not the 17 a 64-bit number can need. The 1e-12 cut is absolute, so coordinates smaller than that are lost. In R12, ellipses and splines become polylines and hatches become outlines.

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