Resources Numerical accuracy Calculation policy

DWG and DXF drawings compiled for the plan view

Turns the entities of a DWG or DXF drawing into the lines, fills and text of each space, in the drawing's own coordinates.

How the precision is kept

Coordinates stay 64-bit: transforms are six-number affine matrices and transformed points are stored in a Float64Array. Curves become straight segments: arcs, circles and ellipses at no more than 5° per segment with at least 8 segments, splines at 8 samples per control point. The segment count, not the arithmetic, sets how close a drawn curve is to the true one.

Calculations under this policy

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

dwg-compile.js BimCraft 0.8.1 64-bit 17 calculations Reviewed

Tests bin/test-bimcraft.mjs, bin/test-takeoffcraft-cad.mjs

Combine two transforms

64-bit

The product of two plan transforms, each six numbers.

[a b c d e f]: x′ = a·x + c·y + e, y′ = b·x + d·y + f

Plain 64-bit products and sums. Nested blocks multiply their transforms one level at a time, so rounding grows with the nesting depth; an insert deeper than 20 levels is not expanded.

  • dwg-compile.js
  • lines 356 to 362
  • BimCraft 0.8.1
function mul(p, q) {
  return [
    p[0] * q[0] + p[2] * q[1], p[1] * q[0] + p[3] * q[1],
    p[0] * q[2] + p[2] * q[3], p[1] * q[2] + p[3] * q[3],
    p[0] * q[4] + p[2] * q[5] + p[4], p[1] * q[4] + p[3] * q[5] + p[5],
  ];
}

Block insert transform

64-bit

Where the content of a block lands: insertion point, rotation, scale, base point and its place in a row and column array.

M = T(insert) · R(rotation) · S(sx, sy) · T(−base + (col × colSpacing ÷ sx, row × rowSpacing ÷ sy))

The array offset is divided by the scale because it sits inside the scale step; the two cancel to the spacing itself, with one division and one multiplication of rounding. A scale of 0 or a missing scale is taken as 1. A tilted insert takes its plan transform from planOcs.

  • dwg-compile.js
  • lines 191 to 199
  • BimCraft 0.8.1
const tilt = planOcs(e.extrusion, e.insert.z || 0);
const im = tilt ? mul(ctx.m, tilt) : m;
for (let r = 0; r < rows; r++) {
  for (let c = 0; c < cols; c++) {
    const local = mul(
      mul(translate(e.insert.x, e.insert.y), rotate(e.rotation || 0)),
      mul(scale(sx, sy), translate(-base.x + c * (e.colSpacing || 0) / sx, -base.y + r * (e.rowSpacing || 0) / sy)),
    );
    const sub = subCtx(ctx, mul(im, local), layer, color, lay, lw, x ? { ...x, prefix: e.block + '|' } : null);

Tilted block seen from above

64-bit

The plan transform of an insert whose extrusion direction is neither up nor down.

Ax = unit(Wz × N) (or Wy × N when |Nx| and |Ny| < 1/64); Ay = unit(N × Ax); offset = N·z

The extrusion is normalized with Math.hypot. Directions shorter than 1e-12, or with |Nx| and |Ny| below 1e-9, return null and are handled as plain up or down. The 1/64 switch is the DXF arbitrary-axis rule. Only the x and y parts of Ax and Ay are kept: the view is from above.

  • dwg-compile.js
  • lines 365 to 380
  • BimCraft 0.8.1
/**
 * A tilted extrusion (neither +Z nor -Z) → the object coordinate system seen from above, as an affine: the arbitrary-axis
 * rule's Ax and Ay projected on XY, and the point's OCS z moved along N. Null for +Z/-Z (`ocs` handles those).
 */
function planOcs(ext, z) {
  if (!ext) return null;
  const l = Math.hypot(ext.x || 0, ext.y || 0, ext.z || 0);
  if (l < 1e-12) return null;
  const N = [(ext.x || 0) / l, (ext.y || 0) / l, (ext.z || 0) / l];
  if (Math.abs(N[0]) < 1e-9 && Math.abs(N[1]) < 1e-9) return null;
  const cross = (a, b) => [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
  const unit = (v) => { const k = Math.hypot(v[0], v[1], v[2]) || 1; return [v[0] / k, v[1] / k, v[2] / k]; };
  const Ax = unit(Math.abs(N[0]) < 1 / 64 && Math.abs(N[1]) < 1 / 64 ? cross([0, 1, 0], N) : cross([0, 0, 1], N));
  const Ay = unit(cross(N, Ax));
  return [Ax[0], Ax[1], Ay[0], Ay[1], N[0] * z, N[1] * z];
}

Downward extrusion

64-bit

Mirrors an entity in X when its extrusion direction points down.

The test is extrusion z below −0.5. An entity whose extrusion is tilted but not past that value is drawn as if it were not tilted; only block inserts get the full rule.

  • dwg-compile.js
  • lines 363 to 364
  • BimCraft 0.8.1
/** Entities drawn with a -Z extrusion are mirrored in X (arbitrary-axis rule, plan views). */
const ocs = (m, ext) => (ext && ext.z < -0.5 ? mul(m, scale(-1, 1)) : m);

Transform points

64-bit

A list of points moved through a transform into the space's coordinates.

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

The result is a Float64Array, so the transformed coordinates keep 64 bits. Two products and two additions per coordinate.

  • dwg-compile.js
  • lines 381 to 389
  • BimCraft 0.8.1
function tx(m, pts) {
  const out = new Float64Array(pts.length);
  for (let i = 0; i < pts.length; i += 2) {
    const x = pts[i], y = pts[i + 1];
    out[i] = m[0] * x + m[2] * y + m[4];
    out[i + 1] = m[1] * x + m[3] * y + m[5];
  }
  return out;
}

Segments in an arc

64-bit

How many straight segments stand in for an arc.

n = max(8, min(128, ⌈|sweep| ÷ 2π × 72⌉))

72 segments to a full turn, so no segment spans more than 5°, and never fewer than 8. This count is what limits how closely the drawn arc follows the true one.

  • dwg-compile.js
  • lines 410 to 411
  • BimCraft 0.8.1
/** Segments for an arc: about 5° each, 8 to 128. */
function segCount(sweep) { return Math.max(8, Math.min(128, Math.ceil(Math.abs(sweep) / TAU * 72))); }

Points along an arc or circle

64-bit

An arc or circle as points on it.

pᵢ = (cx + r·cos aᵢ, cy + r·sin aᵢ), aᵢ = a₀ ± sweep × i ÷ n

A sweep of zero or less has 2π added until it is positive. Every point, the first and last included, is computed from the center, radius and angle; all lie on the circle to 64-bit rounding. Between points the drawing follows chords.

  • dwg-compile.js
  • lines 413 to 423
  • BimCraft 0.8.1
function arcPoints(cx, cy, r, a0, a1, ccw) {
  let sweep = ccw ? a1 - a0 : a0 - a1;
  while (sweep <= 0) sweep += TAU;
  if (a1 - a0 === TAU) sweep = TAU;
  const n = segCount(sweep), dir = ccw ? 1 : -1, pts = [];
  for (let i = 0; i <= n; i++) {
    const a = a0 + dir * sweep * (i / n);
    pts.push(cx + r * Math.cos(a), cy + r * Math.sin(a));
  }
  return pts;
}

Points along an ellipse

64-bit

An ellipse or elliptical arc as points on it.

p(t) = c + R(rot) · (L·cos t, L·ratio·sin t), L = |major axis|, rot = atan2(my, mx)

The major axis direction goes through atan2 and back through cos and sin instead of being used as a unit vector, which adds a rounding step. Points are spaced evenly in the ellipse parameter, not in length.

  • dwg-compile.js
  • lines 425 to 435
  • BimCraft 0.8.1
function ellipsePoints(c, maj, ratio, p0, p1) {
  const mx = maj.x, my = maj.y, len = Math.hypot(mx, my), rot = Math.atan2(my, mx);
  let sweep = p1 - p0;
  while (sweep <= 0) sweep += TAU;
  const n = segCount(sweep), pts = [], cr = Math.cos(rot), sr = Math.sin(rot);
  for (let i = 0; i <= n; i++) {
    const t = p0 + sweep * (i / n), x = len * Math.cos(t), y = len * ratio * Math.sin(t);
    pts.push(c.x + x * cr - y * sr, c.y + x * sr + y * cr);
  }
  return pts;
}

Polyline with bulge arcs

64-bit

A polyline whose segments may be arcs, as points.

θ = 4·atan(bulge); r = chord ÷ (2·sin(θ ÷ 2)); center = chord middle + normal × r·cos(θ ÷ 2)

The vertices are pushed as stored; only the points between them are computed. A bulge of 1e-9 or less is a straight segment. For a very small bulge above that, r is very large and the in-between points are a far-away center plus r, a sum of two large numbers that nearly cancel.

  • dwg-compile.js
  • lines 437 to 464
  • BimCraft 0.8.1
/** Vertices with bulges (LWPOLYLINE, POLYLINE2D, hatch polyline paths) → points. */
function bulgePolyline(vs, closed) {
  const pts = [];
  const 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 (i === n - 1 && !closed) break;
    const bulge = a.bulge || 0;
    if (Math.abs(bulge) > 1e-9) {
      const theta = 4 * Math.atan(bulge), chord = Math.hypot(b.x - a.x, b.y - a.y);
      if (chord > 0) {
        const r = chord / (2 * Math.sin(theta / 2));
        const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2, h = r * Math.cos(theta / 2);
        const ux = -(b.y - a.y) / chord, uy = (b.x - a.x) / chord;
        const cx = mx + ux * h, cy = my + uy * h;
        const a0 = Math.atan2(a.y - cy, a.x - cx);
        const steps = segCount(theta);
        for (let k = 1; k < steps; k++) {
          const t = a0 + theta * (k / steps);
          pts.push(cx + Math.abs(r) * Math.cos(t), cy + Math.abs(r) * Math.sin(t));
        }
      }
    }
    if (i === n - 1 && closed) pts.push(b.x, b.y);
  }
  return pts;
}

Points along a spline

64-bit

A spline as points: its fit points, or samples of the B-spline from its control points.

de Boor: dⱼ = (1 − a)·dⱼ₋₁ + a·dⱼ, a = (u − kᵢ) ÷ (kᵢ₊ₚ₋ᵣ₊₁ − kᵢ); point = (Σ w·P) ÷ Σ w

When the spline has fit points they are returned as they are, joined by straight lines, not the curve through them. Otherwise it is sampled at max(16, 8 × control points) parameters. The last sample is taken at the end minus 1e-9 of the knot range, so the last point is just short of the true end. A zero knot span gives a = 0 instead of a division by zero.

  • dwg-compile.js
  • lines 466 to 493
  • BimCraft 0.8.1
/** Fit points when present, else the B-spline evaluated from its control points (de Boor). */
function splinePoints(e) {
  if (e.fit && e.fit.length > 1) return e.fit.flatMap((p) => [p.x, p.y]);
  const cp = e.control || [], k = e.knots || [], p = e.degree || 3;
  if (cp.length < 2) return [];
  if (k.length !== cp.length + p + 1) return cp.flatMap((q) => [q.x, q.y]);
  const w = e.weights && e.weights.length === cp.length ? e.weights : null;
  const lo = k[p], hi = k[k.length - p - 1], n = Math.max(16, cp.length * 8), pts = [];
  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];
        const 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]];
      }
    }
    pts.push(d[p][0] / d[p][2], d[p][1] / d[p][2]);
  }
  return pts;
}

What a sheet viewport looks at

64-bit

The model point at the middle of a viewport on a sheet.

vx = target_x + cos(tw)·cx + sin(tw)·cy; vy = target_y − sin(tw)·cx + cos(tw)·cy

The view center is turned back by the twist and added to the view target, in 64-bit. The width, height and view height are kept as the file gives them.

  • dwg-compile.js
  • lines 318 to 322
  • BimCraft 0.8.1
const tw = v.twist || 0, c = Math.cos(tw), sn = Math.sin(tw), dx = v.viewCenter?.x || 0, dy = v.viewCenter?.y || 0;
const vp = {
  cx: v.center.x, cy: v.center.y, w: v.width, h: v.height,
  vx: (v.viewTarget?.x || 0) + c * dx + sn * dy, vy: (v.viewTarget?.y || 0) - sn * dx + c * dy,
  viewHeight: v.viewHeight, twist: tw,

A viewport's window on the model

64-bit

The box in model space that a viewport shows, used to say when it looks at nothing.

half width = w ÷ 2 × viewHeight ÷ h; half height = viewHeight ÷ 2; corners turned by the twist

The four corners are turned by the twist and their axis-aligned box is taken, so a twisted viewport's window is larger than what it really shows. The overlap test is exact comparisons with no tolerance.

  • dwg-compile.js
  • lines 391 to 397
  • BimCraft 0.8.1
/** A viewport's window on the model: the axis-aligned box round its four corners (the model shown, turned or not). */
function vpWindow(vp) {
  const hw = vp.w / 2 * vp.viewHeight / vp.h, hh = vp.viewHeight / 2, c = Math.cos(vp.twist), sn = Math.sin(vp.twist);
  const b = emptyBounds();
  for (const [x, y] of [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]]) grow(b, [vp.vx + c * x + sn * y, vp.vy - sn * x + c * y]);
  return b;
}

Picture placement

64-bit

The transform from a picture's pixels to drawing coordinates.

world = P + u·px − v·py + v·h (pixel y runs down; P is the lower left corner)

Built from the picture's two pixel vectors and its corner as the file gives them, then multiplied by the entity's transform. No rounding of its own.

  • dwg-compile.js
  • lines 221 to 222
  • BimCraft 0.8.1
// pixel (x right, y down) → world: the position is the image's lower-left corner
const pm = mul(m, [u.x, u.y, -v.x, -v.y, p.x + h * v.x, p.y + h * v.y]);

Lines of a paragraph of text

64-bit

Where each line of a multi-line text starts.

pitch = h × 1.667 × line spacing; line i at insert + (top − pitch × i) along the up direction

The line pitch uses 1.667, a rounded 5/3. The rotation comes from atan2 of the text's x axis when it has one. Positions are 64-bit.

  • dwg-compile.js
  • lines 552 to 567
  • BimCraft 0.8.1
/** MTEXT (model fields: value, height, insert, attach, xAxis, lineSpacing). */
function mtext(e, m, out, bounds) {
  const lines = mtextLines(e.value).filter((l, i, a) => l.trim() || i < a.length - 1);
  if (!lines.length || !e.height || !e.insert) return;
  const rot = e.xAxis && (e.xAxis.x || e.xAxis.y) ? Math.atan2(e.xAxis.y, e.xAxis.x) : (e.rotation || 0);
  const ap = (e.attach || 1) - 1, ha = ap % 3, row = Math.floor(ap / 3);   // row 0 top, 1 middle, 2 bottom
  const h = e.height, gap = h * 1.667 * (e.lineSpacing || 1);
  const total = gap * (lines.length - 1);
  const top = row === 0 ? -h : row === 1 ? total / 2 - h / 2 : total;   // baseline of the first line, relative to the insertion point
  const cr = Math.cos(rot), sr = Math.sin(rot);
  lines.forEach((ln, i) => {
    if (!ln.trim()) return;
    const dy = top - gap * i;
    place(out, bounds, m, e.insert.x - dy * sr, e.insert.y + dy * cr, h, rot, ln, ha, 0, false);
  });
}

Text position, height and angle

64-bit

One line of text carried through the entity's transform.

H = h × |second column of M|; angle = atan2(m₁, m₀) ± rotation (− when M mirrors)

The height is scaled by the length of the transform's second column and the angle is taken from its first. The width added to the drawing's bounds is an estimate, 0.6 × height per character, used only to fit the view.

  • dwg-compile.js
  • lines 569 to 580
  • BimCraft 0.8.1
/** Put one line of text into the batch, transformed by m (position, height and rotation). */
function place(out, bounds, m, x, y, h, rot, s, ha, va, mirror) {
  const X = m[0] * x + m[2] * y + m[4], Y = m[1] * x + m[3] * y + m[5];
  const sx = Math.hypot(m[0], m[1]), sy = Math.hypot(m[2], m[3]);
  const det = m[0] * m[3] - m[1] * m[2];
  const angle = Math.atan2(m[1], m[0]) + (det < 0 ? -rot : rot);
  const H = h * sy;
  out().texts.push({ x: X, y: Y, h: H, rot: angle, s, ha, va, mirror: mirror !== (det < 0) });
  // Rough extent so fit-to-drawing includes text: assume 0.6 h per character.
  const w = s.length * H * 0.6 * (sx / (sy || 1));
  grow(bounds, [X, Y, X + w * Math.cos(angle), Y + w * Math.sin(angle)]);
}

Paper unit of a sheet

64-bit

Whether a sheet is in inches or millimeters.

unit = mm when the sheet is wider than 100 units, else inches

The unit is not read from the file: it is decided by the sheet's width alone. A sheet in inches wider than 100 inches, or one in millimeters narrower than 100 mm, is taken for the other unit, and printed line weights on it follow that.

  • dwg-compile.js
  • line 333
  • BimCraft 0.8.1
const paperUnit = paper && paper.maxX - paper.minX > 100 ? 'mm' : 'in';

Line weight in millimeters

64-bit

A stored line weight, in hundredths of a millimeter, as millimeters.

mm = v ÷ 100 for 0 ≤ v ≤ 211, else 0.25

One division by 100. Values outside 0 to 211 fall back to the default 0.25 mm.

  • dwg-compile.js
  • line 39
  • BimCraft 0.8.1
const lwOf = (v) => (v >= 0 && v <= 211 ? v / 100 : LW_DEFAULT);
dwg-view.js BimCraft 0.8.1 64-bit 3 calculations Reviewed

Tests None found by the review

Pointer position in drawing coordinates

64-bit

The drawing coordinates under the pointer, for the coordinate readout.

x = cx + (px − W ÷ 2) ÷ zoom; y = cy − (py − H ÷ 2) ÷ zoom

64-bit, from the view center, the zoom and a pixel position. The readout can be no finer than one pixel divided by the zoom. The wheel and the pinch hold the zoom between 1e-9 and 1e9.

  • dwg-view.js
  • lines 152 to 155
  • BimCraft 0.8.1
toWorld(px, py) {
  const W = this.canvas.clientWidth, H = this.canvas.clientHeight;
  return [this.cx + (px - W / 2) / this.zoom, this.cy - (py - H / 2) / this.zoom];
}

Model to sheet through a viewport

64-bit

The transform that carries a model point onto the sheet through one viewport.

k = h ÷ viewHeight; paper = center + k · R(twist) · (model − view point)

The scale is the viewport's height on paper divided by the model height it shows. A model point far from the origin is scaled and then has a translation of nearly the same size taken off it, so its place on the sheet is a difference of two large numbers.

  • dwg-view.js
  • lines 434 to 438
  • BimCraft 0.8.1
/** A viewport's model → paper transform: about the view centre, scaled to the viewport, twisted, moved to its centre. */
function vpMatrix(vp) {
  const k = vp.h / vp.viewHeight, c = Math.cos(vp.twist), sn = Math.sin(vp.twist);
  return [k * c, k * sn, -k * sn, k * c, vp.cx - k * (c * vp.vx - sn * vp.vy), vp.cy - k * (sn * vp.vx + c * vp.vy)];
}

Millimeters per paper unit

Exact

The size of one paper unit of a sheet in millimeters, for line weights.

1 for millimeters, 25.4 for inches

25.4 is the exact definition of the inch. Which unit applies comes from the width guess made when the drawing is compiled.

  • dwg-view.js
  • lines 431 to 432
  • BimCraft 0.8.1
/** Millimetres per paper unit of a sheet (inches unless the compiler found millimetres). */
function mmPer(space) { return space.paperUnit === 'mm' ? 1 : 25.4; }

In detail

Method
Block inserts are expanded with their transforms (insertion point, rotation, scale, base point, rows and columns); an insert nested deeper than 20 levels is counted and not expanded. Bulge arcs are rebuilt from bulge = tan(θ ÷ 4). A spline without fit points is evaluated from its control points and knots by de Boor's algorithm, rational when it has weights.
Number types
64-bit numbers; transformed points in Float64Array.
Tolerances
A bulge of 1e-9 or less is a straight segment. An extrusion direction shorter than 1e-12, or within 1e-9 of the Z axis in x and y, is not treated as tilted. A spline's last sample is taken 1e-9 of the knot range before its end.
Limits
A 5° chord lies inside its arc by r × (1 − cos 2.5°), about 0.1 % of the radius, and a length measured along the chords is about 0.03 % short. A spline with fit points is drawn as straight lines between them. Entities other than block inserts with a tilted extrusion are drawn untilted; only a −Z extrusion is mirrored. A sheet's paper unit is guessed: wider than 100 units is millimeters, otherwise inches.

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