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