Resources Numerical accuracy Calculation policy

DWG import: objects from the file's model

Turns the entities read from a DWG file into drawing objects: dimensions, hatch boundaries, block arrays, pictures, viewports, text angles and lineweights.

How the precision is kept

Coordinates, radii and bulges are copied as the 64-bit numbers the reader gives. Where the file holds a curve the drawing keeps as points, the curve is sampled: arc edges of a hatch boundary at 32 chords per full turn, ellipse edges at 48, bulge arcs at 16 per half turn, spline edges at up to 400 steps. A dimension is kept as an editable dimension only when its recomputed measurement agrees with the file's within 1e-6 × max(1, measurement).

Open item. Keep arc, ellipse and spline edges of a hatch boundary as curves, or sample them to a stated chord tolerance, instead of a fixed number of chords per turn.

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-import.js CadCraft 0.8.2 64-bit 13 calculations Reviewed

Tests bin/test-cadcraft.mjs

Dimension from its defining points

64-bit

Rebuilds a file's dimension as an editable dimension when its points give the measurement the file states.

accept when |measured − file| ≤ 1e-6 × max(1, file); diameter center = (defPoint + chord point) ÷ 2

The measurement is recomputed from the points with Math.hypot or a projection on a candidate angle, and compared with the file's value with a tolerance that is relative above 1 and absolute (1e-6) below. A file with no stored measurement accepts the first candidate. A dimension that fails the test stays a block reference, drawn as the file drew it. Only x and y of the points are used.

  • dwg-import.js
  • lines 254 to 303
  • CadCraft 0.8.2
/** A DWG dimension's defining points (DXF 10 defPoint, 11 textMid, 13/14 xline1/2, 15 center) → a CadCraft dimension, or null. */
function dimension(e) {
  const t = e.dimType ?? 0;
  const P = (p) => (p && Number.isFinite(p.x) && Number.isFinite(p.y) ? { x: p.x, y: p.y } : null);
  const st = { ...(dimStyles.get(String(e.style || '').toUpperCase()) || dimStyle) };
  // per-dimension overrides (ACAD "DSTYLE" xdata: group code, value, …; every variable, dwg-props.js), then what only
  // CadCraft keeps (CADCRAFT "CCDIMST:…": arrowhead names, text style, true colours)
  const ds = dstyleMap(xdataOf(e.xdata, 'ACAD'));
  if (ds.size) stFromCodes((c) => ds.get(c), st);
  ccDimFrom(xdataOf(e.xdata, 'CADCRAFT').map(([, v]) => String(v)), st);
  if (!(st.scale > 0)) st.scale = 1;
  lunitOk(st);
  const meas = typeof e.measurement === 'number' && e.measurement > 0 ? e.measurement : null;
  const close = (v) => meas === null || Math.abs(v - meas) <= 1e-6 * Math.max(1, meas);
  const text = mtextPlain(String(e.text || '')).replace(/^<>$/, '');
  const tp = P(e.textMid);
  const textAt = tp && (tp.x || tp.y) ? tp : null;
  let d = null;
  if (t === 0 || t === 1) {
    const p1 = P(e.xline1), p2 = P(e.xline2), q2 = P(e.defPoint);
    if (!p1 || !p2 || !q2 || dist(p1, p2) < 1e-12) return null;
    if (t === 1) d = close(dist(p1, p2)) ? { kind: 'aligned', p1, p2, loc: q2 } : null;
    else {
      // the dimension line's angle: as recorded (DXF 50), else square to the line from p2 to its point on the line. The
      // recorded one first (0.6 DF.1): the definition point may sit on either extension line (ezdxf puts it on the first),
      // and a file that has not worked out its measurement (-1, as the ODA File Converter writes) cannot rule a guess out.
      const cands = [normAngle(e.angle || 0)];
      if (dist(p2, q2) > 1e-9) cands.push(normAngle(Math.atan2(q2.y - p2.y, q2.x - p2.x) + Math.PI / 2));
      cands.push(0, Math.PI / 2);
      const rot = cands.find((r) => close(Math.abs((p2.x - p1.x) * Math.cos(r) + (p2.y - p1.y) * Math.sin(r))));
      d = rot === undefined ? null : { kind: 'linear', p1, p2, loc: q2, rot };
    }
  } else if (t === 4 || t === 3) {
    const a = P(e.defPoint), p1 = P(e.center);
    if (!a || !p1) return null;
    const c = t === 4 ? a : { x: (a.x + p1.x) / 2, y: (a.y + p1.y) / 2 };
    const r = dist(c, p1);
    if (r < 1e-12 || !close(t === 4 ? r : 2 * r)) return null;
    d = { kind: t === 4 ? 'radius' : 'diameter', c, p1, loc: textAt || p1 };
  } else if (t === 5) {
    const c = P(e.center), p1 = P(e.xline1), p2 = P(e.xline2), loc = P(e.defPoint);
    if (c && p1 && p2 && loc && dist(c, p1) > 1e-12 && dist(c, p2) > 1e-12 && dist(c, loc) > 1e-12) d = { kind: 'angular', c, p1, p2, loc };
  }
  if (!d) return null;
  const out = { ...d, text, st };
  if (e.style && !/^standard$/i.test(e.style)) out.dimstyle = String(e.style);
  if (Number.isFinite(e.textRotation) && Math.abs(e.textRotation) > 1e-12) out.trot = normAngle(e.textRotation);
  if ((t === 0 || t === 1) && Number.isFinite(e.obliqueAngle) && Math.abs(e.obliqueAngle) > 1e-9) out.obl = normAngle(e.obliqueAngle);   // slanted extension lines (DIMEDIT Oblique, 0.7 D7.14)
  return out;
}

Hatch boundary to polygon rings

64-bit

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

arc: n = max(4, ⌈32 × sweep ÷ 2π⌉) chords; ellipse: n = max(8, ⌈48 × sweep ÷ 2π⌉); p = c + r × (cos a, sin a)

Arc and ellipse edges are replaced by equal-angle chords, so the stored boundary is an inscribed polygon: at 32 chords per turn its area is 0.64 percent below a full circle's. The sample points themselves are 64-bit and lie on the curve. A ring needs at least 3 points to be kept.

  • dwg-import.js
  • lines 838 to 876
  • CadCraft 0.8.2
function hatchLoops(e) {
  const rings = [];
  for (const path of e.loops || []) {
    const pts = [];
    if (path.polyline) {
      const vs = path.polyline.vertices || [], n = vs.length;
      for (let i = 0; i < n; i++) {
        const a = vs[i], b = vs[(i + 1) % n];
        pts.push(a.x, a.y);
        if (Math.abs(a.bulge || 0) > 1e-9) arcBetween(pts, a, b, a.bulge);
      }
      if (n) pts.push(vs[0].x, vs[0].y);
    } else {
      for (const ed of path.edges || []) {
        if (ed.type === 'line') pts.push(ed.start.x, ed.start.y, ed.end.x, ed.end.y);
        else if (ed.type === 'arc') {
          let s = ed.startAngle, t = ed.endAngle;
          if (ed.ccw === false) { s = -s; t = -t; }   // a clockwise edge keeps its angles measured clockwise
          let sweep = ed.ccw === false ? s - t : t - s;
          while (sweep <= 0) sweep += TAU;
          const dir = ed.ccw === false ? -1 : 1, n = Math.max(4, Math.ceil(32 * sweep / TAU));
          for (let q = 0; q <= n; q++) { const a = s + dir * sweep * q / n; pts.push(ed.center.x + ed.radius * Math.cos(a), ed.center.y + ed.radius * Math.sin(a)); }
        } else if (ed.type === 'ellipse') {
          const len = Math.hypot(ed.majorAxis.x, ed.majorAxis.y) || 1, ratio = ed.ratio || 1, rot = Math.atan2(ed.majorAxis.y, ed.majorAxis.x);
          let sweep = ed.endAngle - ed.startAngle; while (sweep <= 0) sweep += TAU;
          const n = Math.max(8, Math.ceil(48 * sweep / TAU));
          for (let q = 0; q <= n; q++) {
            const t = ed.startAngle + sweep * q / n, x = len * Math.cos(t), y = len * ratio * Math.sin(t);
            pts.push(ed.center.x + x * Math.cos(rot) - y * Math.sin(rot), ed.center.y + x * Math.sin(rot) + y * Math.cos(rot));
          }
        } else if (ed.type === 'spline') {
          for (const p of splinePoints({ fit: ed.fit, control: ed.control, knots: ed.knots, degree: ed.degree, weights: ed.weights })) pts.push(p.x, p.y);
        }
      }
    }
    if (pts.length >= 6) rings.push(pts);
  }
  return rings;
}

Bulge arc to points

64-bit

The points along a polyline arc segment given by its two ends and its bulge.

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

Closed form from the chord length c and the bulge; the chords span at most 11.25 degrees. A zero chord returns nothing. The division by sin(θ ÷ 2) is not guarded: it only nears zero for a bulge so large that the arc is almost a full circle.

  • dwg-import.js
  • lines 878 to 885
  • CadCraft 0.8.2
function arcBetween(pts, p, q, b) {
  const theta = 4 * Math.atan(b), c = Math.hypot(q.x - p.x, q.y - p.y);
  if (!c) return;
  const r = c / (2 * Math.sin(theta / 2)), mx = (p.x + q.x) / 2, my = (p.y + q.y) / 2, h = r * Math.cos(theta / 2);
  const ux = -(q.y - p.y) / c, uy = (q.x - p.x) / c, cx = mx + ux * h, cy = my + uy * h;
  const a0 = Math.atan2(p.y - cy, p.x - cx), n = Math.max(2, Math.ceil(16 * Math.abs(theta) / Math.PI));
  for (let k = 1; k < n; k++) { const t = a0 + theta * k / n; pts.push(cx + Math.abs(r) * Math.cos(t), cy + Math.abs(r) * Math.sin(t)); }
}

Spline edge to points

64-bit

Points along a spline edge of a hatch boundary.

n = min(400, max(16, 8 × control points)); de Boor: d_j = (1 − a) × d_{j−1} + a × d_j, a = (u − k_i) ÷ (k_{i+p−r+1} − k_i); point = (x, y) ÷ w

Evaluated at n + 1 equally spaced parameters with weights carried as a third coordinate. The last parameter is pulled in by 1e-9 × the knot range so it stays inside the last span. A zero knot difference uses a = 0. When the knot count does not fit the control points, the fit points or the control points are used as the outline instead.

  • dwg-import.js
  • lines 812 to 836
  • CadCraft 0.8.2
function splinePoints(e) {
  // a fit-point spline with a tolerance: the reader's curve through the fit points (approx*), for drawing
  if (!(e.control && e.control.length) && e.approxControl && e.approxKnots) e = { ...e, control: e.approxControl, knots: e.approxKnots, weights: null };
  const cp = e.control || [], k = e.knots || [], p = e.degree || 3;
  // 0.6.1: the curve itself when its control points and knots are known (the reader computes them for a fit-point
  // spline when it can) — the fit points alone are only a coarse outline of it; they are the fallback
  if (!(cp.length >= 2 && k.length === cp.length + p + 1) && e.fit && e.fit.length > 1) return e.fit;
  if (cp.length < 2) return [];
  if (k.length !== cp.length + p + 1) return cp;
  const w = e.weights && e.weights.length === cp.length ? e.weights : null;
  const lo = k[p], hi = k[k.length - p - 1], n = Math.min(400, Math.max(16, cp.length * 8)), out = [];
  for (let s = 0; s <= n; s++) {
    const u = s === n ? hi - 1e-9 * (hi - lo) : lo + (hi - lo) * (s / n);
    let span = p;
    while (span < cp.length - 1 && u >= k[span + 1]) span++;
    const d = [];
    for (let j = 0; j <= p; j++) { const q = cp[span - p + j], wt = w ? w[span - p + j] : 1; d.push([q.x * wt, q.y * wt, wt]); }
    for (let r = 1; r <= p; r++) for (let j = p; j >= r; j--) {
      const i = span - p + j, den = k[i + p - r + 1] - k[i], a = den === 0 ? 0 : (u - k[i]) / den;
      d[j] = [(1 - a) * d[j - 1][0] + a * d[j][0], (1 - a) * d[j - 1][1] + a * d[j][1], (1 - a) * d[j - 1][2] + a * d[j][2]];
    }
    out.push({ x: d[p][0] / d[p][2], y: d[p][1] / d[p][2] });
  }
  return out;
}

Picture and wipeout frame

64-bit

The corners of an image's frame or clip boundary in drawing coordinates.

W(q) = p + (q_x + 0.5) × u + (h − 0.5 − q_y) × v

An affine map from pixel coordinates (y down) with the half-pixel offsets of the DXF reference: the insertion point is the lower-left corner, u and v are one pixel along and up. Sums of products in 64-bit; 0.5 is exact.

  • dwg-import.js
  • lines 375 to 378
  • CadCraft 0.8.2
const W = (q) => ({ x: p.x + (q.x + 0.5) * u.x + (h - 0.5 - q.y) * v.x, y: p.y + (q.x + 0.5) * u.y + (h - 0.5 - q.y) * v.y, b: 0 });
let clip = e.clipping && Array.isArray(e.clip) && e.clip.length >= 2 ? e.clip : [{ x: -0.5, y: -0.5 }, { x: w - 0.5, y: h - 0.5 }];
if (clip.length === 2) { const [a, b] = clip; clip = [{ x: a.x, y: a.y }, { x: b.x, y: a.y }, { x: b.x, y: b.y }, { x: a.x, y: b.y }]; }
const frame = { type: 'poly', closed: true, pts: clip.map(W) };

Block array positions

64-bit

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

(x, y) = insert + R(rot) × (k × column spacing, r × row spacing)

Each offset is index × spacing, a product and not a running sum, so the last copy is as exact as the first. Cosine and sine of the rotation are taken once.

  • dwg-import.js
  • lines 569 to 574
  • CadCraft 0.8.2
const cols = Math.max(1, e.cols || 1), rows = Math.max(1, e.rows || 1);
const c = Math.cos(ins.rot), s = Math.sin(ins.rot);
for (let r = 0; r < rows; r++) for (let k = 0; k < cols; k++) {
  const dx = k * (e.colSpacing || 0), dy = r * (e.rowSpacing || 0);
  out.push({ ...ins, x: ins.x + dx * c - dy * s, y: ins.y + dx * s + dy * c });
}

Viewport scale

64-bit

The scale of a sheet viewport and the model point at its center.

s = viewport height ÷ view height (1 when the view height is not above 0)

One 64-bit division. The scale is stored as this quotient, not as a named ratio such as 1:50.

  • dwg-import.js
  • lines 611 to 613
  • CadCraft 0.8.2
const s = e.viewHeight > 0 ? e.height / e.viewHeight : 1;
const mc = vpModelCentre(dc, t, Number.isFinite(e.twist) ? e.twist : 0);   // the view centre is turned by the twist (DCS)
const vp = { type: 'viewport', ...base, x: c.x, y: c.y, w: e.width, h: e.height, cx: mc.x, cy: mc.y, s };

Hatch pattern lines

64-bit

The file's own pattern lines: angle in degrees, base point, and offset along and across each line.

angle° = angle × 180 ÷ π; dx = o_x cos a + o_y sin a; dy = −o_x sin a + o_y cos a

The offset is rotated from world axes into the line's own frame with the angle in radians as read. The angle is also stored in degrees, a multiplication by 180 and a division by π. A hatch with more than 64 line families keeps none of them, and each family keeps at most 32 dashes.

  • dwg-import.js
  • lines 557 to 561
  • CadCraft 0.8.2
const pl = (e.patternLines || []).filter((l) => [l.angle, l.base?.x, l.base?.y, l.offset?.x, l.offset?.y].every(Number.isFinite));
if (pl.length && pl.length <= 64) h.patlines = pl.map((l) => {
  const ca = Math.cos(l.angle), sa = Math.sin(l.angle);
  return { angle: l.angle * 180 / Math.PI, x: l.base.x, y: l.base.y, dx: l.offset.x * ca + l.offset.y * sa, dy: -l.offset.x * sa + l.offset.y * ca, dashes: (l.dashes || []).filter(Number.isFinite).slice(0, 32) };
});

Lineweight in millimeters

64-bit

Converts a lineweight from hundredths of a millimeter to millimeters.

mm = v ÷ 100 for v ≥ 0; −2 and −3 kept as codes

The file holds a whole number of hundredths, so the format sets the resolution at 0.01 mm. The division gives the nearest 64-bit number to that decimal.

  • dwg-import.js
  • line 92
  • CadCraft 0.8.2
const lwMm = (v) => (v === -2 || v === -3 ? v : Number.isFinite(v) && v >= 0 ? v / 100 : undefined);   // 1/100 mm → mm; -1 by layer = absent

Multi-line text rotation and oblique angle

64-bit

The rotation of multi-line text from its direction vector, and its oblique angle from the text's format code.

rot = atan2(xAxis_y, xAxis_x); oblique = degrees in the \Q code × π ÷ 180

The rotation is exact to atan2. The oblique angle is parsed from decimal text inside the string, so it carries only the digits written there; this app writes it with 6 decimals of a degree.

  • dwg-import.js
  • lines 534 to 538
  • CadCraft 0.8.2
const rot = e.xAxis && (e.xAxis.x || e.xAxis.y) ? Math.atan2(e.xAxis.y, e.xAxis.x) : 0;
const ap = (e.attach || 1) - 1;
const ww = e.width > 0 ? e.width : 0, ls = e.lineSpacing > 0 && e.lineSpacing !== 1 ? e.lineSpacing : 0;
const q = /^\\Q(-?[\d.]+);/.exec(String(e.value || ''));   // an oblique angle as the text's first format code (how CadCraft writes it)
const obl = q && Number.isFinite(+q[1]) && +q[1] ? +q[1] * Math.PI / 180 : 0;

Construction line direction

64-bit

The unit direction of an infinite line or ray.

n = √(d_x² + d_y²); direction = (d_x ÷ n, d_y ÷ n)

The file's direction is normalized again with Math.hypot. A direction shorter than 1e-12 (absolute) is skipped and counted.

  • dwg-import.js
  • lines 629 to 631
  • CadCraft 0.8.2
const d = e.direction, n = d ? Math.hypot(d.x, d.y) : 0;
if (!e.start || !Number.isFinite(e.start.x) || !(n > 1e-12)) { skip(e.type); return null; }
return { type: type === 'XLINE' ? 'xline' : 'ray', ...base, x: e.start.x, y: e.start.y, dx: d.x / n, dy: d.y / n };   // world coordinates (no OCS)

Dimension style numbers

64-bit

Reads a dimension style's variables and brings the decimal places and flags into range.

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

Sizes are copied as 64-bit numbers. The number of decimal places is rounded to a whole number and capped at 8, so a file asking for more places shows 8.

  • dwg-import.js
  • lines 105 to 111
  • CadCraft 0.8.2
const styleFrom = (src, base) => {
  const st = stFromVars(src, { ...base });   // every variable CadCraft has a key for (dwg-props.js)
  if (!(st.scale > 0)) st.scale = 1;
  for (const k of ['dec', 'adec', 'tad', 'zin']) st[k] = Math.max(0, Math.min(8, Math.round(st[k] || 0)));
  lunitOk(st);
  return st;
};

Which viewport is the sheet

64-bit

Picks the viewport that stands for the sheet itself: at scale 1 and covering every other viewport.

|s − 1| ≤ 1e-6; covers: |o_x − v_x| + o_w ÷ 2 ≤ v_w ÷ 2 + 1e-6 × max(1, v_w, v_h)

The cover test is scaled to the viewport's size (at least 1 unit); the scale test is absolute. The result decides which viewport is dropped, not any stored number.

  • dwg-import.js
  • lines 887 to 903
  • CadCraft 0.8.2
/**
 * The viewport that is the sheet itself, among a layout's converted paper-space entities (D7.18), or undefined.
 * CadCraft's own record (xdata) wins; else it is the viewport that shows paper space 1:1 and covers every other viewport
 * (the largest such); else, in a layout with several, the first one when it covers all the others. A lone viewport at
 * another scale is a real one and stays. (The old rule took the first viewport found, which hid a real one listed first.)
 */
export function sheetViewport(list) {
  const vps = list.filter((e) => e.type === 'viewport');
  const own = vps.find((v) => v.cc);
  if (own) return own;
  const tol = (v) => 1e-6 * Math.max(1, v.w, v.h);
  const covers = (v, o) => Math.abs(o.x - v.x) + o.w / 2 <= v.w / 2 + tol(v) && Math.abs(o.y - v.y) + o.h / 2 <= v.h / 2 + tol(v);
  const all = (v) => vps.every((o) => o === v || covers(v, o));
  const unit = vps.filter((v) => Math.abs(v.s - 1) <= 1e-6 && all(v)).sort((a, b) => b.w * b.h - a.w * a.h);
  if (unit.length) return unit[0];
  return vps.length > 1 && all(vps[0]) ? vps[0] : undefined;
}

In detail

Method
Direct copy of doubles; closed-form arc from a bulge (θ = 4 atan b); de Boor evaluation of spline edges; pixel-to-world affine map for picture frames.
Number types
64-bit numbers throughout. Lineweights arrive as whole hundredths of a millimeter and are divided by 100.
Tolerances
Dimension measurement 1e-6 × max(1, measurement); zero lengths 1e-12 (absolute); a bulge counts as an arc above 1e-9; the sheet's own viewport is recognized within 1e-6 × max(1, width, height) and a scale within 1e-6 of 1.
Limits
Hatch boundaries with arcs, ellipses or splines become polygons, so a hatch's outline and area are those of the polygon. A full circle becomes 32 chords, an inscribed polygon whose area is 0.64 percent below the circle's (16 × sin(π ÷ 16) ÷ π = 0.9936).

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