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.
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.
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.
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.
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;
};
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.