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