Resources Numerical accuracy Calculation policy

DWG and DXF drawings in 3D

Turns a drawing's model space into 3D lines, faces and text, with every block insert, elevation, thickness and extrusion direction applied.

How the precision is kept

Transforms are 3 × 4 matrices of 64-bit numbers and the batches are Float64Array. Object coordinate systems are built from the extrusion direction by the arbitrary-axis rule. As in the plan view, curves are straight segments at no more than 5° each and splines are sampled, so the segment count sets how closely a curve is followed.

Calculations under this policy

12 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-compile3d.js BimCraft 0.8.1 64-bit 10 calculations Reviewed

Tests bin/test-bimcraft.mjs

Combine two 3D transforms

64-bit

The product of two 3 × 4 transforms.

Three products and two or three additions per term, in 64-bit. Nested blocks multiply one level at a time; an insert deeper than 20 levels is not expanded.

  • dwg-compile3d.js
  • lines 48 to 58
  • BimCraft 0.8.1
export function mul3(a, b) {
  const r = new Array(12);
  for (let i = 0; i < 3; i++) {
    const o = i * 4;
    r[o] = a[o] * b[0] + a[o + 1] * b[4] + a[o + 2] * b[8];
    r[o + 1] = a[o] * b[1] + a[o + 1] * b[5] + a[o + 2] * b[9];
    r[o + 2] = a[o] * b[2] + a[o + 1] * b[6] + a[o + 2] * b[10];
    r[o + 3] = a[o] * b[3] + a[o + 1] * b[7] + a[o + 2] * b[11] + a[o + 3];
  }
  return r;
}

Transform a point

64-bit

A point carried through a 3 × 4 transform.

x′ = m₀·x + m₁·y + m₂·z + m₃ (and the same for y′, z′)

One 64-bit expression per coordinate. The translation is added last, after the three products.

  • dwg-compile3d.js
  • line 62
  • BimCraft 0.8.1
export const apply3 = (m, x, y, z) => [m[0] * x + m[1] * y + m[2] * z + m[3], m[4] * x + m[5] * y + m[6] * z + m[7], m[8] * x + m[9] * y + m[10] * z + m[11]];

Extrusion direction

64-bit

The unit extrusion direction of an entity, or straight up when it has none.

N = (x, y, z) ÷ √(x² + y² + z²)

Normalized with Math.hypot. A direction shorter than 1e-12 (absolute) is replaced by (0, 0, 1).

  • dwg-compile3d.js
  • lines 65 to 69
  • BimCraft 0.8.1
/** The extrusion direction (DXF 210), or Z when it is missing or zero. */
export function normal(ext) {
  const x = ext?.x || 0, y = ext?.y || 0, z = ext?.z || 0, l = Math.hypot(x, y, z);
  return l < 1e-12 ? [0, 0, 1] : [x / l, y / l, z / l];
}

Object coordinate system

64-bit

The transform from an entity's own plane to world coordinates.

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

The identity is returned only when N is exactly (0, 0, 1); a direction a few rounding steps off Z goes through the cross products instead, which gives the same axes to rounding. The 1/64 switch is the DXF arbitrary-axis rule.

  • dwg-compile3d.js
  • lines 70 to 79
  • BimCraft 0.8.1
/** Object coordinate system → world: AutoCAD's arbitrary-axis algorithm (columns Ax, Ay, N). */
export function ocsMatrix(ext) {
  const N = normal(ext);
  if (N[0] === 0 && N[1] === 0 && N[2] === 1) return I3;
  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 l = Math.hypot(v[0], v[1], v[2]) || 1; return [v[0] / l, v[1] / l, v[2] / l]; };
  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], Ay[0], N[0], 0, Ax[1], Ay[1], N[1], 0, Ax[2], Ay[2], N[2], 0];
}

Thickness vector

64-bit

The offset between the bottom and top of an entity that has thickness.

up = M · (N × thickness), without the translation

The unit extrusion direction times the thickness, carried through the linear part of the transform. A result of exactly zero length gives no thickness.

  • dwg-compile3d.js
  • lines 255 to 260
  • BimCraft 0.8.1
const thick = (t, ext) => {
  if (!t) return null;
  const N = normal(ext);
  const v = dir3(m, N[0] * t, N[1] * t, N[2] * t);
  return Math.hypot(v[0], v[1], v[2]) > 0 ? v : null;
};

Block insert transform in 3D

64-bit

Where the content of a block lands in 3D, with rows and columns.

M = OCS · T(insert) · Rz(rotation) · T(col × colSpacing, row × rowSpacing, 0) · S(sx, sy, sz) · T(−base)

Five matrix products per insert, each in 64-bit. The array offset is applied outside the scale, so the spacing is used as stored.

  • dwg-compile3d.js
  • lines 401 to 403
  • BimCraft 0.8.1
const local = mul3(mul3(T3(ip.x, ip.y, ip.z), Rz(e.rotation)),
  mul3(T3(k * e.colSpacing, r * e.rowSpacing, 0), mul3(S3(s.x, s.y, s.z), T3(-base.x, -base.y, -base.z))));
const sub = { m: mul3(om, local), layer, color, layerColor: lay.color, depth: ctx.depth + 1, owner: ctx.owner,

Ellipse in 3D

64-bit

An ellipse or elliptical arc as 3D points.

minor = (N × major) × ratio; p(t) = c + cos t · major + sin t · minor

The minor axis is the cross product of the unit extrusion direction and the major axis, times the ratio. The center and major axis are used as world coordinates. At most 5° of parameter per segment, at least 8 segments.

  • dwg-compile3d.js
  • lines 362 to 370
  • BimCraft 0.8.1
const N = normal(e.extrusion), mj = e.majorAxis, c0 = e.center;
const mn = [(N[1] * mj.z - N[2] * mj.y) * e.ratio, (N[2] * mj.x - N[0] * mj.z) * e.ratio, (N[0] * mj.y - N[1] * mj.x) * e.ratio];
let sweep = e.endParam - e.startParam;
while (sweep <= 0) sweep += TAU;
const n = Math.max(8, Math.min(128, Math.ceil(sweep / TAU * 72))), pts = [];
for (let i = 0; i <= n; i++) {
  const t = e.startParam + sweep * (i / n), co = Math.cos(t), si = Math.sin(t);
  pts.push(apply3(m, c0.x + co * mj.x + si * mn[0], c0.y + co * mj.y + si * mn[1], c0.z + co * mj.z + si * mn[2]));
}

Half width of a wide polyline

64-bit

The offset to each side of a polyline that has a start and end width.

w(k) = (sw + (ew − sw) × length so far ÷ segment length) ÷ 2, along the normal of the path at k

The width is interpolated by length along the segment's points. The normal at a point is taken from its two neighbors, so on an arc it is the chord direction, not the true tangent. A zero-length direction is divided by 1 instead of 0.

  • dwg-compile3d.js
  • lines 285 to 290
  • BimCraft 0.8.1
const side = (k) => {   // half the width at point k, along the normal of the path there
  const w = ((a.sw || 0) + ((b === a ? 0 : (a.ew || 0)) - (a.sw || 0)) * (total ? at2[k] / total : 0)) / 2;
  const p = P2[Math.max(0, k - 1)], q = P2[Math.min(P2.length - 1, k + 1)];
  const dx = q[0] - p[0], dy = q[1] - p[1], l = Math.hypot(dx, dy) || 1;
  return [-dy / l * w, dx / l * w];
};

Spline in 3D

64-bit

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

de Boor on (w·x, w·y, w·z, w); point = (Σ w·P) ÷ Σ w

The same method as the plan view with a z coordinate added. Fit points are returned as they are. The last sample is taken 1e-9 of the knot range before the end, and a zero knot span gives a = 0 instead of a division by zero.

  • dwg-compile3d.js
  • lines 444 to 468
  • BimCraft 0.8.1
/** A spline through its fit points, else its B-spline from the control points (de Boor), in 3D. */
export function spline3(e) {
  const P = (q) => [q.x, q.y, q.z || 0];
  const fit = e.fit || e.fitPoints || [], cp = e.control || e.controlPoints || [], k = e.knots || [], p = e.degree || 3;
  if (fit.length > 1) return fit.map(P);
  if (cp.length < 2) return [];
  if (k.length !== cp.length + p + 1) return cp.map(P);
  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, (q.z || 0) * 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] = d[j].map((v, c) => (1 - a) * d[j - 1][c] + a * v);
      }
    }
    pts.push([d[p][0] / d[p][3], d[p][1] / d[p][3], d[p][2] / d[p][3]]);
  }
  return pts;
}

Extents of the drawing

64-bit

The smallest box round everything drawn, in drawing units.

Minimum and maximum of the transformed points by exact comparison. A point with a coordinate that is not finite is left out, and so is the segment or face it belongs to.

  • dwg-compile3d.js
  • lines 206 to 210
  • BimCraft 0.8.1
const grow = (p) => {
  for (let k = 0; k < 3; k++) { if (!Number.isFinite(p[k])) return false; }
  for (let k = 0; k < 3; k++) { if (p[k] < bmin[k]) bmin[k] = p[k]; if (p[k] > bmax[k]) bmax[k] = p[k]; }
  return true;
};
dwg-view3d.js BimCraft 0.8.1 64-bit 2 calculations Reviewed

Tests tools/bimcraft-test/browser-3d.mjs

Section plane offset

64-bit

Where a section plane cuts the drawing, from a typed offset in drawing units.

at = lo + clamp(offset × s, 0, hi − lo); offset shown = (at − lo) ÷ s, s = 100 ÷ largest extent

The offset is turned into scene units by the view's scale, clamped to the drawing's box and turned back. Both steps are 64-bit; the box used here is kept in ordinary numbers, not in the 32-bit arrays sent to the GPU.

  • dwg-view3d.js
  • lines 301 to 306
  • BimCraft 0.8.1
const lo = this.box.min[i], hi = this.box.max[i];
const at = lo + Math.max(0, Math.min(hi - lo, (Number(s.offset) || 0) * this.scale));
const sign = (s.flip ? -1 : 1) * keep;   // keep: −1 = keep the low side (x ≤ at), +1 = the high side
const n = new THREE.Vector3().setComponent(i, sign);
const plane = new THREE.Plane(n, -sign * at);   // kept: n·p + c ≥ 0
this.section = { axis: k, offset: (at - lo) / this.scale, flip: !!s.flip, plane };

Section range

64-bit

The largest offset a section plane can take along an axis, in drawing units.

size = (max − min) ÷ s

The scene box divided back by the scale it was multiplied by, in 64-bit. The result can differ from the drawing's own extent in the last digit.

  • dwg-view3d.js
  • lines 319 to 324
  • BimCraft 0.8.1
/** [0, size] along the axis, in drawing units. */
sectionRange(axis) {
  if (!this.box) return [0, 0];
  const { i } = SECTION[axis] || SECTION.top;
  return [0, (this.box.max[i] - this.box.min[i]) / this.scale];
}

In detail

Method
Each entity is placed by its own object coordinate system (arbitrary-axis rule on the extrusion direction), then by the transforms of the block inserts above it. Thickness is added along the extrusion direction. Splines without fit points are evaluated in 3D by de Boor's algorithm.
Number types
64-bit numbers; lines and triangles in Float64Array; owner indexes in Uint32Array.
Tolerances
An extrusion direction shorter than 1e-12 is taken as +Z. The object coordinate system is the identity only when the normalized direction is exactly (0, 0, 1). A spline's last sample is taken 1e-9 of the knot range before its end.
Limits
Arcs, circles and ellipses are chords of at most 5°. A spline with fit points is drawn as straight lines between them. ACIS solids, surfaces and subdivision meshes are counted, not drawn.

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