Resources Numerical accuracy Calculation policy

DWG save: objects to the file's model

Turns drawing objects into the entities a DWG file holds: dimensions with their defining points, hatches with pattern lines, text, viewports, sheets, lineweights and extents.

How the precision is kept

Coordinates, radii, bulges, angles and scales are handed to the DWG writer as the 64-bit numbers the drawing holds. Two kinds of value are rounded here on the way: lineweights to whole hundredths of a millimeter (the format's unit) and the oblique angle of multi-line text to 6 decimals of a degree inside its format code. Pattern lines are scaled, turned and moved in 64-bit with 25.4 as the inch factor.

Open item. Write the oblique angle of multi-line text with full precision instead of 6 decimals of a degree.

Calculations under this policy

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

dwg-save.js CadCraft 0.8.2 64-bit 13 calculations Reviewed

Tests bin/test-cadcraft.mjs

Lineweight in hundredths of a millimeter

64-bit

Converts a lineweight in millimeters to the whole number the file stores.

Math.round(w × 100) for w ≥ 0; negative codes kept

The value kept in the file is rounded to 0.01 mm because the format stores a whole number. A lineweight that is not a whole number of hundredths comes back changed.

  • dwg-save.js
  • lines 93 to 94
  • CadCraft 0.8.2
// linetypes and lineweights (0.4 C8): DWG keeps lineweights in 1/100 mm (-1 by layer, -2 by block, -3 default)
const lw100 = (w, absent) => (w === undefined || w === null ? absent : w < 0 ? w : Math.round(w * 100));

Spline: fit points or control points

64-bit

Decides whether a spline's fit points are written with it, by checking that they still give its control points.

every |recomputed − stored| ≤ 1e-7 × max(1, max(|x| + |y|))

The control points are recomputed from the fit points and compared with a tolerance scaled to the largest coordinate. Control points, knots and weights are written as they are either way; only the fit points and a flag depend on the test.

  • dwg-save.js
  • lines 167 to 170
  • CadCraft 0.8.2
const fitOnly = !!e.fit && !e.closed && !e.periodic && e.fit.length >= 2 && (() => {
  const n = fitSpline(e.fit, { st: e.st, et: e.et, degree: e.degree }), tol = 1e-7 * Math.max(1, ...e.ctrl.map((p) => Math.abs(p.x) + Math.abs(p.y)));
  return !!n && n.ctrl.length === e.ctrl.length && n.ctrl.every((p, i) => Math.hypot(p.x - e.ctrl[i].x, p.y - e.ctrl[i].y) <= tol);
})();

Multi-line text anchor and oblique angle

64-bit

Moves baseline-anchored text to a top anchor one text height up, and writes the oblique angle as a format code.

anchor = (x, y) + R(rot) × (0, h); code = \Q followed by degrees to 6 decimals

The anchor shift is one rotation of (0, h) in 64-bit. The oblique angle is converted to degrees and rounded with toFixed(6) before it is written into the text, so it reads back within 5e-7 degrees and not to 15 digits.

  • dwg-save.js
  • lines 274 to 277
  • CadCraft 0.8.2
let va = e.va || 'baseline', at = { x: e.x, y: e.y };
if (!(va in ROW)) { const d = mApply(mRotate(e.rot || 0), { x: 0, y: e.h }); at = { x: e.x + d.x, y: e.y + d.y }; va = 'top'; }
let body = rich ? e.mt : s.split('\n').map((l) => l.replace(/\\/g, '\\\\').replace(/\{/g, '\\{').replace(/\}/g, '\\}')).join('\\P');
if (e.obl && !(rich && /^\\Q/.test(e.mt))) body = '\\Q' + +(e.obl * 180 / Math.PI).toFixed(6) + ';' + body;   // MTEXT has no oblique field: its format code (P.7)

Hatch boundary rings

64-bit

Writes a hatch's rings as closed polylines, or a 3 or 4 cornered solid fill as a SOLID.

Ring coordinates are copied; every vertex is written with bulge 0. The repeated closing point is dropped first (openRing, 1e-12 absolute). A SOLID's corners are written in the order 1, 2, 4, 3 the format expects.

  • dwg-save.js
  • lines 292 to 308
  • CadCraft 0.8.2
/** Solid fill → HATCH SOLID (a 3- or 4-cornered single ring → SOLID); a pattern → HATCH with the pattern's own lines. */
function hatch(e, base) {
  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] }); return openRing(pts); }).filter((r) => r.length >= 3);
  if (!rings.length) return [];
  const z = Number.isFinite(e.z) ? e.z : 0;
  if (e.solid && rings.length === 1 && rings[0].length <= 4 && !e.wipe) {
    const [a, b, c, d = c] = rings[0];
    return [{ ...base, type: 'SOLID', points: [P(a.x, a.y, z), P(b.x, b.y, z), P(d.x, d.y, z), P(c.x, c.y, z)], ...(z ? { elevation: z } : {}) }];   // SOLID corners run 1, 2, 4, 3
  }
  const loops = rings.map((r, i) => ({ flags: i ? 0 : 1, polyline: { closed: true, vertices: r.map((p) => ({ x: p.x, y: p.y, bulge: 0 })) } }));
  const more = { ...(z ? { elevation: z } : {}), style: ISLAND[e.island] ?? 0 };   // island detection (P.7): normal 0, outer 1, ignore 2
  if (e.solid) return [{ ...base, type: 'HATCH', pattern: 'SOLID', solid: true, loops, ...more }];
  const lines = patternLines(e);
  if (!lines) note('hatchUnknown');
  return [{ ...base, type: 'HATCH', pattern: String(e.pattern || 'USER').toUpperCase().slice(0, 31), solid: false, patternType: PATTERNS[String(e.pattern || '').toUpperCase()] ? 1 : 2,
    angle: e.pangle || 0, scale: e.pscale > 0 ? e.pscale : 1, loops, lines: lines || [], ...more }];
}

Hatch pattern lines for the file

64-bit

The pattern's line families in drawing units: angle in radians, base point and offset in world axes.

k = scale ÷ (1 metric, 25.4 otherwise); base = R(turn) × (x, y) × k + origin; offset = (dx cos a − dy sin a, dx sin a + dy cos a)

25.4 is exact. The family angle is kept in degrees, has the pattern's turn added as turn × 180 ÷ π, and is converted back with × π ÷ 180, two rounded conversions. The pattern origin is added to every base point.

  • dwg-save.js
  • lines 309 to 325
  • CadCraft 0.8.2
/** The pattern's line families in drawing units, as DWG keeps them (angle in radians, base, offset in world axes). */
function patternLines(e) {
  let fams = null;
  if (Array.isArray(e.patlines) && e.patlines.length) fams = e.patlines;
  else {
    const def = PATTERNS[String(e.pattern || '').toUpperCase()];
    if (!def) return null;
    const k = (e.pscale > 0 ? e.pscale : 1) / (hatchIsMetric(e, doc) ? 1 : 25.4), turn = e.pangle || 0;
    const c = Math.cos(turn), s = Math.sin(turn);
    fams = def.lines.map((f) => ({ angle: f.angle + turn * 180 / Math.PI, x: (f.x * c - f.y * s) * k, y: (f.x * s + f.y * c) * k, dx: f.dx * k, dy: f.dy * k, dashes: f.dashes.map((d) => d * k) }));
  }
  const ox = Number.isFinite(e.ox) ? e.ox : 0, oy = Number.isFinite(e.oy) ? e.oy : 0;   // the pattern's origin (P.7) moves every line, as hatch.js draws it
  return fams.map((f) => {
    const a = f.angle * Math.PI / 180, ca = Math.cos(a), sa = Math.sin(a);
    return { angle: a, base: { x: f.x + ox, y: f.y + oy }, offset: { x: f.dx * ca - f.dy * sa, y: f.dx * sa + f.dy * ca }, dashes: (f.dashes || []).slice() };
  });
}

Dimension points and measurement

64-bit

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

measurement = dimMeasure; linear: defPoint = 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)

The measurement is the full 64-bit value from dims.js, not the rounded text. For radius and diameter the point on the curve is recomputed from r and the angle to the text point, so it lies on the circle to rounding but is not the point that was picked.

  • dwg-save.js
  • lines 327 to 362
  • CadCraft 0.8.2
/** A dimension → DIMENSION (its points in DXF meaning) + the unnamed block that draws it. */
function dimension(e, base) {
  const own = e.block && doc.blocks.get(e.block);
  const name = '*D' + ++dimN;
  blocks.push({ name, base: P(0, 0), entities: (own ? own.entities : dimGeometry(e)).flatMap(conv) });
  note('dim');
  const st = { ...DIM_DEFAULTS, ...(e.st || {}) };
  const d = { ...base, type: 'DIMENSION', dimType: DIM_TYPE[e.kind] ?? 0, block: name, style: dimStyleName(e.dimstyle), textMid: P(...xy(dimTextPoint(e))),
    text: e.text || '', measurement: dimMeasure(e), attach: 5, ...(e.trot ? { textRotation: e.trot } : {}) };
  d.flags = d.dimType | 32 | 128;   // 32: its block is its own; 128: the text sits where it was put
  switch (e.kind) {
    case 'linear': case 'aligned': {
      const { q2, r } = dimLinePoints(e);   // where the dimension line meets the second extension line (slanted ones included)
      Object.assign(d, { defPoint: P(q2.x, q2.y), xline1: P(e.p1.x, e.p1.y), xline2: P(e.p2.x, e.p2.y), angle: e.kind === 'linear' ? r : 0, ...(Number.isFinite(e.obl) && e.obl ? { obliqueAngle: e.obl } : {}) });
      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 = P(e.c.x + r * Math.cos(a), e.c.y + r * Math.sin(a)), far = P(e.c.x - r * Math.cos(a), e.c.y - r * Math.sin(a));
      Object.assign(d, { defPoint: e.kind === 'radius' ? P(e.c.x, e.c.y) : far, center: near, leaderLen: 0 });
      break;
    }
    case 'angular':
      Object.assign(d, { defPoint: P(e.loc.x, e.loc.y), xline1: P(e.p1.x, e.p1.y), xline2: P(e.p2.x, e.p2.y), center: P(e.c.x, e.c.y) });
      break;
    default: note('other'); return [{ ...base, type: 'INSERT', block: name, insert: P(0, 0) }];
  }
  // this dimension's own settings, as AutoCAD keeps per-dimension overrides (ACAD "DSTYLE" xdata: code, value, …), every
  // variable one for one (dwg-props.js); what no variable holds exactly (arrowhead names, text style, true colours) in CADCRAFT
  d.xdata = { ACAD: dstyleItems(st) };
  const cc = ccDimItems(st);
  if (cc.length) d.xdata.CADCRAFT = cc;
  return [d];
}

Viewport view height

64-bit

The model height a sheet viewport shows, and its view center.

view height = h ÷ s (s taken as 1 when not above 0)

One 64-bit division of the viewport's paper height by its scale. The reader divides the other way (h ÷ view height), so a scale goes out and comes back through two divisions and can differ in its last digit.

  • dwg-save.js
  • lines 387 to 397
  • CadCraft 0.8.2
/** A sheet's viewport → VIEWPORT (paper units; the model view at its centre and scale). */
function viewport(e, base, id = 0) {
  // status (DXF 90): 0x8060 as AutoCAD writes it; + 16384 display locked, + 131072 viewport off (P.7)
  const status = 0x8060 | (e.locked ? 16384 : 0) | (e.on === false ? 131072 : 0);
  const frz = Array.isArray(e.frz) ? e.frz.filter((n) => typeof n === 'string' && n) : [];   // layers frozen in it (VP.1): names, cc-dwg finds the records
  // its layer overrides (VP.3) go into the layers' XRECORDs, which name this viewport by handle
  const lov = !modelOnly && e.lov && typeof e.lov === 'object' && Object.keys(e.lov).length ? e.lov : null;
  const vh = lov ? 'CC' + (0x7C000000 + vpOverrides.length).toString(16).toUpperCase() : null;
  if (lov) vpOverrides.push({ h: vh, lov });
  return { ...base, ...(vh ? { handle: vh } : {}), ...(Number.isFinite(e.tw) && e.tw ? { twist: e.tw } : {}), type: 'VIEWPORT', center: P(e.x, e.y), width: e.w, height: e.h, viewCenter: dcsOf(e), viewTarget: P(0, 0), viewHeight: e.h / (e.s > 0 ? e.s : 1), id, status, ...(frz.length ? { frozenLayers: frz } : {}) };
}

Sheet paper size and limits

64-bit

The paper size in millimeters, the sheet's limits and the sheet viewport's center.

paper mm = w × 25.4 (inch sheets) or w; limits = (x₀, y₀) to (x₀ + w, y₀ + h); center = (x₀ + w ÷ 2, y₀ + h ÷ 2)

25.4 is exact. The paper's corner and size are also written unchanged as 64-bit values in the sheet's own record, so they do not depend on the millimeter conversion.

  • dwg-save.js
  • lines 461 to 474
  • CadCraft 0.8.2
const p = L.paper, inch = p.unit === 'in', mm = inch ? 25.4 : 1;
const record = 'CC' + (i + 1);
const sheet = viewport({ x: p.x0 + p.w / 2, y: p.y0 + p.h / 2, w: p.w, h: p.h, cx: p.x0 + p.w / 2, cy: p.y0 + p.h / 2, s: 1 }, { layer: '0', color: color(null) }, 1);
// CadCraft's own record of the sheet (as in its DXF): name, paper unit and size key, the paper's corner and size
sheet.xdata = { CADCRAFT: [[1000, 'LAYOUT'], [1000, String(L.name).slice(0, 255)], [1000, p.unit], [1000, p.key || ''], [1040, p.x0], [1040, p.y0], [1040, p.w], [1040, p.h]] };
let vpId = 1;
const ents = [sheet, ...[...L.entities.values()].flatMap((e) => (e.type === 'viewport' ? [viewport(e, { layer: e.layer || '0', color: color(e.color) }, ++vpId)] : conv(e)))];
spaces.push({ name: i ? '*Paper_Space' + (i - 1) : '*Paper_Space', record, base: P(0, 0), entities: ents });
layouts.push({
  name: L.name, tabOrder: i + 1, block: record,
  paper: { name: p.key || (inch ? 'Custom_(' + p.w + '_x_' + p.h + '_Inches)' : 'Custom_(' + p.w + '_x_' + p.h + '_MM)'), w: p.w * mm, h: p.h * mm,
    units: inch ? 0 : 1, rotation: 0, plotType: 5, scaleType: 16, scale: 1, left: 0, bottom: 0, right: 0, top: 0,
    ...(L.plotStyle ? { styleSheet: styleSheetName(L.plotStyle) } : {}) },
  limits: { min: { x: p.x0, y: p.y0 }, max: { x: p.x0 + p.w, y: p.y0 + p.h } }, insbase: P(0, 0),

Drawing extents

64-bit

The smallest and largest x and y of everything in model space, written to the file's header.

min and max over the bounds of every object

Comparisons only; the result is one of the objects' own bound values. The bounds of each object come from geom.js.

  • dwg-save.js
  • lines 478 to 482
  • CadCraft 0.8.2
const ext = emptyBounds();
for (const e of doc.model.values()) {
  const b = entityBounds(e, doc.blocks);
  if (validBounds(b)) { ext.minX = Math.min(ext.minX, b.minX); ext.minY = Math.min(ext.minY, b.minY); ext.maxX = Math.max(ext.maxX, b.maxX); ext.maxY = Math.max(ext.maxY, b.maxY); }
}

Closing point of a ring

64-bit

Drops a ring's last point when it repeats the first.

|first − last| < 1e-12

The test is absolute, 1e-12 drawing units. At coordinates above about 8200 neighboring 64-bit numbers are farther apart than that, so there only an exactly repeated point is dropped.

  • dwg-save.js
  • lines 595 to 600
  • CadCraft 0.8.2
/** A closed ring's distinct corners (the repeated closing point dropped). */
function openRing(pts) {
  const out = pts.slice();
  if (out.length > 1 && Math.hypot(out[0].x - out[out.length - 1].x, out[0].y - out[out.length - 1].y) < 1e-12) out.pop();
  return out;
}

Construction line direction

64-bit

The unit direction written for an infinite line or ray.

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

Normalized with Math.hypot at save time. A zero direction writes nothing.

  • dwg-save.js
  • lines 153 to 155
  • CadCraft 0.8.2
const n = Math.hypot(e.dx, e.dy);
if (!(n > 0) || !Number.isFinite(e.x) || !Number.isFinite(e.y)) return [];
return [{ ...base, type: e.type === 'xline' ? 'XLINE' : 'RAY', start: P(e.x, e.y, z), direction: P(e.dx / n, e.dy / n) }];

User coordinate system axes

64-bit

The origin and the x and y axis vectors of a user coordinate system.

x axis = (cos θ, sin θ); y axis = (−sin θ, cos θ)

The drawing keeps a UCS as an origin and an angle; the file keeps axis vectors, so the angle goes through cosine and sine and comes back through atan2 when read.

  • dwg-save.js
  • lines 491 to 492
  • CadCraft 0.8.2
// the current UCS and the named ones (DR.8; ucs.js { x, y, ang }): $UCSORG / $UCSXDIR / $UCSYDIR and the UCS table
const axes = (u) => ({ origin: P(u.x, u.y), xAxis: P(Math.cos(u.ang), Math.sin(u.ang)), yAxis: P(-Math.sin(u.ang), Math.cos(u.ang)) });

Linetype pattern length

64-bit

The total length of one repeat of a linetype's dashes and gaps.

length = Σ |elementᵢ|

The absolute values are added in pattern order as 64-bit numbers.

  • dwg-save.js
  • lines 529 to 532
  • CadCraft 0.8.2
const linetypes = [...usedLt].map((name) => {
  const own = doc.linetypes?.get(name), pattern = patternOf(doc, name) || [];
  return { name, ...(ltHandles.has(name) ? { handle: ltHandles.get(name) } : {}), description: own?.description || name, patternLength: pattern.reduce((a, v) => a + Math.abs(v), 0), elements: pattern.map((length) => ({ length })) };
});

In detail

Method
Direct copy of doubles; dimension points recomputed from the dimension's defining points; pattern line families scaled by pattern scale ÷ (1 or 25.4) and rotated.
Number types
64-bit numbers; lineweights as whole numbers of 1/100 mm.
Tolerances
A spline is written with fit data only when recomputed control points match within 1e-7 × max(1, largest |x| + |y|); a ring's repeated closing point is dropped within 1e-12 (absolute); a radial dimension's direction falls back to the picked point when the text point is within 1e-12 of the center.
Limits
Hatch boundaries are written as polygons with no bulges, as the drawing holds them.

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