Resources Numerical accuracy Calculation policy

Import of DXF and DWG drawings into the 3D model

The edges and faces made when a DXF or DWG drawing is brought into the 3D model as a site plan: unit scaling, block placement, and arcs, circles, ellipses and splines turned into straight segments.

How the precision is kept

Coordinates are read as 64-bit numbers, transformed by the block placements in the drawing's own unit, and multiplied once by the unit factor to meters. Curves are not kept as curves: arcs, circles and ellipses become 48 straight segments per full turn and splines 12 per knot span, so the sampling sets how closely the result follows the drawing. Line work is laid flat at z = 0; 3D faces keep their heights. The resulting points are then welded on a 1e-5 m grid (see Welding of imported geometry).

Open item. Choose the number of segments from the radius and a chord tolerance instead of a fixed 48 per turn.

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.

m3d/io.js CadCraft 0.8.2 64-bit 15 calculations Reviewed

Tests None found by the review

Drawing unit factors

Exact

The factor from the drawing's unit ($INSUNITS) to meters.

in 0.0254; ft 0.3048; mi 1609.344; mm 0.001; cm 0.01; m 1; km 1000; yd 0.9144

Each factor is the exact defined value of the unit in meters, written as a decimal literal. A unit code that is not in this table has no entry, and the drawing is then read as if it stated no unit.

  • m3d/io.js
  • line 532
  • CadCraft 0.8.2
const DXF_UNITS = { 1: ['in', 0.0254], 2: ['ft', 0.3048], 3: ['mi', 1609.344], 4: ['mm', 0.001], 5: ['cm', 0.01], 6: ['m', 1], 7: ['km', 1000], 10: ['yd', 0.9144] };

Which unit factor is used

64-bit

Picks the factor for a drawing.

The drawing's own unit code is used when it is in the table. Otherwise the unit the person chose is used, and if that is missing the drawing is read as meters (factor 1).

  • m3d/io.js
  • lines 969 to 970
  • CadCraft 0.8.2
let units = null, k = UNIT_M[opts.unit] || 1;
if (DXF_UNITS[plan.insUnits]) { units = DXF_UNITS[plan.insUnits][0]; k = DXF_UNITS[plan.insUnits][1]; }

Mirrored entities

64-bit

How an entity drawn in a flipped coordinate system is read.

x → −x when the extrusion direction's z is negative

Only the sign of group 230 (the z of the extrusion direction) is read; when it is negative, x is negated. An extrusion direction that is tilted rather than straight up or down is not applied.

  • m3d/io.js
  • lines 608 to 609
  • CadCraft 0.8.2
const f = num(g, 230, 1) < 0;
const X = (x) => (f ? -x : x);

Arc angles

64-bit

The start and end angles of a DXF arc.

radians = degrees × π ÷ 180; mirrored: (a₀, a₁) → (π − a₁, π − a₀)

Each angle is parsed from text and converted with one multiplication and one division. A mirrored arc swaps and reflects its two angles.

  • m3d/io.js
  • lines 629 to 634
  • CadCraft 0.8.2
case 'ARC': {
  let a0 = num(g, 50) * Math.PI / 180, a1 = num(g, 51) * Math.PI / 180;
  if (f) [a0, a1] = [Math.PI - a1, Math.PI - a0];
  const r = num(g, 40);
  return r > 0 ? { t: 'arc', c: [X(num(g, 10)), num(g, 20)], r, a0, a1 } : NONE;
}

Placing and scaling a segment

64-bit

Moves each line segment by the block placements above it and converts it to meters.

x′ = a·x + c·y + e; y′ = b·x + d·y + f; then × k

The 2D affine is applied in the drawing's unit and the result multiplied by the unit factor, one multiplication per coordinate. z is set to 0. A segment whose two ends come out equal is dropped, and a coordinate of 1e7 m or more stops the import.

  • m3d/io.js
  • lines 976 to 986
  • CadCraft 0.8.2
// 2D affine [a, b, c, d, e, f]: x' = a x + c y + e, y' = b x + d y + f
const T = (m, p) => [m[0] * p[0] + m[2] * p[1] + m[4], m[1] * p[0] + m[3] * p[1] + m[5]];
const Tmul = (m, n) => [m[0] * n[0] + m[2] * n[1], m[1] * n[0] + m[3] * n[1], m[0] * n[2] + m[2] * n[3], m[1] * n[2] + m[3] * n[3], m[0] * n[4] + m[2] * n[5] + m[4], m[1] * n[4] + m[3] * n[5] + m[5]];
const seg = (m, p, q) => {
  const a = T(m, p), b = T(m, q);
  if (a[0] === b[0] && a[1] === b[1]) return;
  if (!(Number.isFinite(a[0]) && Number.isFinite(a[1]) && Number.isFinite(b[0]) && Number.isFinite(b[1]))) return;
  if (Math.max(Math.abs(a[0]), Math.abs(a[1]), Math.abs(b[0]), Math.abs(b[1])) * k >= 1e7) throw new Error(t('m3d.io.this_drawing_reaches_more_than_10'));
  if (segs.length >= MAX_SEGS) throw new Error(t('m3d.io.this_drawing_has_more_than_2'));
  segs.push([[a[0] * k, a[1] * k, 0], [b[0] * k, b[1] * k, 0]]);
};

Arcs, circles and bulges as segments

64-bit

The points that stand for an arc, a circle or a polyline bulge.

n = max(2, ⌈|sweep| ÷ (2π ÷ 48)⌉); pⱼ = c + r (cos aⱼ, sin aⱼ), aⱼ = a₀ + sweep × j ÷ n; bulge: sweep = 4 atan b

Each point's angle is computed from j directly. With 48 segments per turn each chord's middle is r × (1 − cos(π ÷ 48)), about 0.21 % of the radius, inside the true arc. For a bulge the center is found from the chord and (1 ÷ b − b) ÷ 2; a very small bulge puts the center far away, and each point is then a large center coordinate plus a large, nearly opposite radius term.

  • m3d/io.js
  • lines 987 to 999
  • CadCraft 0.8.2
const arcPts = (c, r, a0, sweep) => {
  const n = Math.max(2, Math.ceil(Math.abs(sweep) / (2 * Math.PI / CIRCLE_SEGS)));
  const out = [];
  for (let j = 0; j <= n; j++) { const a = a0 + (sweep * j) / n; out.push([c[0] + r * Math.cos(a), c[1] + r * Math.sin(a)]); }
  return out;
};
const bulgePts = (p, q, b) => {
  if (!b || Math.abs(b) < 1e-12) return [p, q];
  const s = (1 / b - b) / 2;
  const c = [(p[0] + q[0]) / 2 - s * (q[1] - p[1]) / 2, (p[1] + q[1]) / 2 + s * (q[0] - p[0]) / 2];
  const r = Math.hypot(p[0] - c[0], p[1] - c[1]);
  return arcPts(c, r, Math.atan2(p[1] - c[1], p[0] - c[0]), 4 * Math.atan(b));
};

Ellipse as segments

64-bit

The points that stand for an ellipse or an elliptical arc.

p = c + major × cos t + minor × sin t, minor = major turned 90° × ratio

Sampled at equal steps of the ellipse parameter, 48 per full turn and at least 4, each t computed from its index. The curve is full when the sweep is within 1e-9 of 2π.

  • m3d/io.js
  • lines 847 to 858
  • CadCraft 0.8.2
/** An ellipse (WCS centre, major axis, ratio, parameters) as a sampled curve; flip = extrusion facing down. */
function ellipseCurve(c, maj, ratio, t0, t1, flip) {
  if (!(Math.hypot(maj[0], maj[1]) > 0) || !(ratio > 0)) return NONE;
  const mn = flip ? [maj[1] * ratio, -maj[0] * ratio] : [-maj[1] * ratio, maj[0] * ratio];
  let sw = t1 - t0;
  while (sw <= 0) sw += 2 * Math.PI;
  const full = Math.abs(sw - 2 * Math.PI) < 1e-9;
  const n = Math.max(4, Math.ceil(sw / (2 * Math.PI / CIRCLE_SEGS)));
  const pts = [];
  for (let k = 0; k <= (full ? n - 1 : n); k++) { const t = t0 + (sw * k) / n; pts.push([c[0] + maj[0] * Math.cos(t) + mn[0] * Math.sin(t), c[1] + maj[1] * Math.cos(t) + mn[1] * Math.sin(t)]); }
  return { t: 'curve', kind: 'ellipse', pts, closed: full };
}

Spline (NURBS) as segments

64-bit

The points that stand for a spline with control points.

de Boor on homogeneous points (x·w, y·w, w); 12 samples per knot span

Evaluated with the de Boor recurrence in 64-bit; a zero knot interval gives a blend factor of 0 instead of a division. The last sample is taken at the end knot minus 1e-12 of the knot range, not at the end knot itself.

  • m3d/io.js
  • lines 891 to 919
  • CadCraft 0.8.2
function nurbsPts(p, U, P, W) {
  const n = P.length;
  if (!(p >= 1) || n < p + 1 || U.length !== n + p + 1 || P.some((q) => !q.every(Number.isFinite)) || !U.every(Number.isFinite)) return null;
  const a = U[p], b = U[n];
  if (!(b > a)) return null;
  const point = (u) => {
    let k = p;
    while (k < n - 1 && u >= U[k + 1]) k++;
    const d = [];
    for (let j = 0; j <= p; j++) { const q = P[k - p + j], w = W ? W[k - p + j] || 1 : 1; d.push([q[0] * w, q[1] * w, w]); }
    for (let r = 1; r <= p; r++) {
      for (let j = p; j >= r; j--) {
        const i = k - p + j, den = U[i + p - r + 1] - U[i];
        const al = den > 0 ? (u - U[i]) / den : 0;
        d[j] = [0, 1, 2].map((m) => (1 - al) * d[j - 1][m] + al * d[j][m]);
      }
    }
    const w = d[p][2] || 1;
    return [d[p][0] / w, d[p][1] / w];
  };
  const out = [];
  for (let k = p; k < n; k++) {
    const u0 = U[k], u1 = U[k + 1];
    if (!(u1 > u0)) continue;
    for (let j = 0; j < SPLINE_PER_SPAN; j++) out.push(point(u0 + ((u1 - u0) * j) / SPLINE_PER_SPAN));
  }
  out.push(point(b - (b - a) * 1e-12));
  return out.length >= 2 && out.length < 200000 ? out : null;
}

Spline stored with fit points only

64-bit

The points that stand for a spline that carries fit points and no control points.

centripetal Catmull–Rom, 12 samples per span

The original curve is not rebuilt: a centripetal Catmull–Rom curve through the fit points is used, and the code's own comment says it is close to, not the same as, the source program's fit. Knot steps are the square root of the point spacing with a floor of 1e-12.

  • m3d/io.js
  • lines 866 to 890
  • CadCraft 0.8.2
/**
 * A smooth curve through fit points (a spline stored with fit points only, as some writers save it): centripetal
 * Catmull–Rom, SPLINE_PER_SPAN points per span. Close to, not the same as, the program's own fit.
 */
function fitCurve(F, closed) {
  const n = F.length;
  if (n < 3) return F.slice();
  const at = (i) => (closed ? F[(i + n) % n] : i < 0 ? [2 * F[0][0] - F[1][0], 2 * F[0][1] - F[1][1]] : i >= n ? [2 * F[n - 1][0] - F[n - 2][0], 2 * F[n - 1][1] - F[n - 2][1]] : F[i]);
  const out = [];
  const spans = closed ? n : n - 1;
  for (let i = 0; i < spans; i++) {
    const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2);
    const tj = (a, b) => Math.max(1e-12, Math.sqrt(Math.hypot(b[0] - a[0], b[1] - a[1])));
    const t1 = tj(p0, p1), t2 = t1 + tj(p1, p2), t3 = t2 + tj(p2, p3);
    for (let k = 0; k < SPLINE_PER_SPAN; k++) {
      const t = t1 + ((t2 - t1) * k) / SPLINE_PER_SPAN;
      const L = (a, b, ta, tb) => [0, 1].map((m) => ((tb - t) * a[m] + (t - ta) * b[m]) / (tb - ta));
      const A1 = L(p0, p1, 0, t1), A2 = L(p1, p2, t1, t2), A3 = L(p2, p3, t2, t3);
      const B1 = L(A1, A2, 0, t2), B2 = L(A2, A3, t1, t3);
      out.push(L(B1, B2, t1, t2));
    }
  }
  if (!closed) out.push(F[n - 1]);
  return out;
}

Bulge arcs in hatch boundaries

64-bit

The points of a curved side of a hatch boundary given as a polyline bulge.

center = chord midpoint + ((1 ÷ b − b) ÷ 2) × (chord turned 90°) ÷ 2; sweep = 4 atan b

The same bulge formula and the same 48 segments per turn as for polylines; a bulge below 1e-12 in size is a straight side.

  • m3d/io.js
  • lines 949 to 952
  • CadCraft 0.8.2
const sc = (1 / bu - bu) / 2, c = [(a[0] + b[0]) / 2 - sc * (b[1] - a[1]) / 2, (a[1] + b[1]) / 2 + sc * (b[0] - a[0]) / 2];
const r = Math.hypot(a[0] - c[0], a[1] - c[1]), st = Math.atan2(a[1] - c[1], a[0] - c[0]), sw = 4 * Math.atan(bu);
const n = Math.max(2, Math.ceil(Math.abs(sw) / (2 * Math.PI / CIRCLE_SEGS)));
for (let k = 0; k < n; k++) put([c[0] + r * Math.cos(st + (sw * k) / n), c[1] + r * Math.sin(st + (sw * k) / n)]);

Block insert transform

64-bit

The transform that places a block, with its scale, rotation and array spacing.

M = T(x, y) · R(rot) · S(s_x, s_y) · T(−base); array offset = R(rot) · (col × d_c, row × d_r)

cos and sin of the rotation are computed once per insert and reused. Nested inserts multiply their 2D transforms, up to 16 levels deep, so each level adds the rounding of one product.

  • m3d/io.js
  • lines 1112 to 1120
  • CadCraft 0.8.2
const cs = Math.cos(e.rot), sn = Math.sin(e.rot);
// translate(x,y) · rotate · scale · translate(−base)
const RS = [cs * e.sx, sn * e.sx, -sn * e.sy, cs * e.sy, e.x, e.y];
const local = Tmul(RS, [1, 0, 0, 1, -b.base[0], -b.base[1]]);
const cols = Math.max(1, e.cols || 1), rows = Math.max(1, e.rows || 1);
for (let r = 0; r < Math.min(rows, 1000); r++) for (let c = 0; c < Math.min(cols, 1000); c++) {
  const off = [cs * c * e.dc - sn * r * e.dr, sn * c * e.dc + cs * r * e.dr];
  draw(b.ents, Tmul(m, Tmul([1, 0, 0, 1, off[0], off[1]], local)), depth + 1);
}

Hatch holes

64-bit

Which loops of a hatch are outlines and which are holes.

area = ½ |Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)|; a loop inside an odd number of larger loops is a hole

Loops are ordered by shoelace area and tested by an even-odd crossing count of their first point against each larger loop, with no tolerance; a first point that lies on another loop's edge can fall either way.

  • m3d/io.js
  • lines 1074 to 1082
  • CadCraft 0.8.2
// which loops are holes: a loop inside an odd number of others
const L = e.loops.map((pts) => ({ pts, A: Math.abs(area2(pts)) })).sort((x, y) => y.A - x.A);
const outers = [];
for (const l of L) {
  const inside = L.filter((o) => o !== l && o.A > l.A && inPoly(l.pts[0], o.pts));
  if (inside.length % 2 === 0) outers.push({ l, holes: [] });
  else { const host = inside.sort((x, y) => x.A - y.A)[0]; const o = outers.find((q) => q.l === host); if (o) o.holes.push(l); }
}
for (const o of outers) polys.push({ loops: [o.l, ...o.holes].map((q) => q.pts.map((p) => P3(m, [p[0], p[1], 0]))) });

3D face corners

64-bit

The corners of 3D faces and solids, which keep their heights.

(x, y) placed by the block transform; all three × k

x and y go through the 2D block transform and the unit factor; z is only multiplied by the unit factor, so a block's own transform does not act on heights.

  • m3d/io.js
  • line 1001
  • CadCraft 0.8.2
const P3 = (m, p) => { const a = T(m, p); return [a[0] * k, a[1] * k, (p[2] || 0) * k]; };

Text line pitch

64-bit

The distance between lines of imported multi-line text, which is drawn as edges.

pitch = height × 1.667 × spacing factor

1.667 is a constant written to three decimals.

  • m3d/io.js
  • line 1004
  • CadCraft 0.8.2
const lh = e.h * 1.667 * (e.spacing || 1);

Edges of imported solids

64-bit

How the edges of an imported solid are matched to decide which ones show.

key = round(x × 1e5), round(y × 1e5), round(z × 1e5)

Corner positions in meters are compared as text keys rounded to 1e-5 m, the same grid the importer welds on.

  • m3d/io.js
  • line 1167
  • CadCraft 0.8.2
const vk = (p) => p.map((x) => Math.round(x * 1e5)).join(',');

In detail

Method
2D affine transforms for block inserts; bulge arcs from the chord and bulge; de Boor evaluation of NURBS; centripetal Catmull–Rom for splines stored with fit points only; even-odd test for hatch holes.
Number types
64-bit numbers.
Tolerances
A bulge below 1e-12 is a straight segment. A full ellipse is a sweep within 1e-9 of 2π. Repeated points closer than 1e-12 (drawing units) are dropped. A coordinate of 1e7 m or more stops the import.
Limits
A 48-segment circle's chords lie up to 0.21 % of the radius inside the true circle (1 − cos(π ÷ 48)). Only the sign of an entity's extrusion direction is used. Drawing unit codes other than inches, feet, miles, millimeters, centimeters, meters, kilometers and yards are treated as not stated.

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