Turns the entities read from a DWG file into drawing objects: dimensions, hatch boundaries, block arrays, pictures, viewports, text angles and lineweights.
How the precision is kept
Coordinates, radii and bulges are copied as the 64-bit numbers the reader gives. Where the file holds a curve the drawing keeps as points, the curve is sampled: arc edges of a hatch boundary at 32 chords per full turn, ellipse edges at 48, bulge arcs at 16 per half turn, spline edges at up to 400 steps. A dimension is kept as an editable dimension only when its recomputed measurement agrees with the file's within 1e-6 × max(1, measurement).
Open item. Keep arc, ellipse and spline edges of a hatch boundary as curves, or sample them to a stated chord tolerance, instead of a fixed number of chords per turn.
Calculations under this policy
13 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.
Rebuilds a file's dimension as an editable dimension when its points give the measurement the file states.
accept when |measured − file| ≤ 1e-6 × max(1, file); diameter center = (defPoint + chord point) ÷ 2
The measurement is recomputed from the points with Math.hypot or a projection on a candidate angle, and compared with the file's value with a tolerance that is relative above 1 and absolute (1e-6) below. A file with no stored measurement accepts the first candidate. A dimension that fails the test stays a block reference, drawn as the file drew it. Only x and y of the points are used.
dwg-import.js
lines 254 to 303
CadCraft 0.8.2
/** A DWG dimension's defining points (DXF 10 defPoint, 11 textMid, 13/14 xline1/2, 15 center) → a CadCraft dimension, or null. */
function dimension(e) {
const t = e.dimType ?? 0;
const P = (p) => (p && Number.isFinite(p.x) && Number.isFinite(p.y) ? { x: p.x, y: p.y } : null);
const st = { ...(dimStyles.get(String(e.style || '').toUpperCase()) || dimStyle) };
// per-dimension overrides (ACAD "DSTYLE" xdata: group code, value, …; every variable, dwg-props.js), then what only
// CadCraft keeps (CADCRAFT "CCDIMST:…": arrowhead names, text style, true colours)
const ds = dstyleMap(xdataOf(e.xdata, 'ACAD'));
if (ds.size) stFromCodes((c) => ds.get(c), st);
ccDimFrom(xdataOf(e.xdata, 'CADCRAFT').map(([, v]) => String(v)), st);
if (!(st.scale > 0)) st.scale = 1;
lunitOk(st);
const meas = typeof e.measurement === 'number' && e.measurement > 0 ? e.measurement : null;
const close = (v) => meas === null || Math.abs(v - meas) <= 1e-6 * Math.max(1, meas);
const text = mtextPlain(String(e.text || '')).replace(/^<>$/, '');
const tp = P(e.textMid);
const textAt = tp && (tp.x || tp.y) ? tp : null;
let d = null;
if (t === 0 || t === 1) {
const p1 = P(e.xline1), p2 = P(e.xline2), q2 = P(e.defPoint);
if (!p1 || !p2 || !q2 || dist(p1, p2) < 1e-12) return null;
if (t === 1) d = close(dist(p1, p2)) ? { kind: 'aligned', p1, p2, loc: q2 } : null;
else {
// the dimension line's angle: as recorded (DXF 50), else square to the line from p2 to its point on the line. The
// recorded one first (0.6 DF.1): the definition point may sit on either extension line (ezdxf puts it on the first),
// and a file that has not worked out its measurement (-1, as the ODA File Converter writes) cannot rule a guess out.
const cands = [normAngle(e.angle || 0)];
if (dist(p2, q2) > 1e-9) cands.push(normAngle(Math.atan2(q2.y - p2.y, q2.x - p2.x) + Math.PI / 2));
cands.push(0, Math.PI / 2);
const rot = cands.find((r) => close(Math.abs((p2.x - p1.x) * Math.cos(r) + (p2.y - p1.y) * Math.sin(r))));
d = rot === undefined ? null : { kind: 'linear', p1, p2, loc: q2, rot };
}
} else if (t === 4 || t === 3) {
const a = P(e.defPoint), p1 = P(e.center);
if (!a || !p1) return null;
const c = t === 4 ? a : { x: (a.x + p1.x) / 2, y: (a.y + p1.y) / 2 };
const r = dist(c, p1);
if (r < 1e-12 || !close(t === 4 ? r : 2 * r)) return null;
d = { kind: t === 4 ? 'radius' : 'diameter', c, p1, loc: textAt || p1 };
} else if (t === 5) {
const c = P(e.center), p1 = P(e.xline1), p2 = P(e.xline2), loc = P(e.defPoint);
if (c && p1 && p2 && loc && dist(c, p1) > 1e-12 && dist(c, p2) > 1e-12 && dist(c, loc) > 1e-12) d = { kind: 'angular', c, p1, p2, loc };
}
if (!d) return null;
const out = { ...d, text, st };
if (e.style && !/^standard$/i.test(e.style)) out.dimstyle = String(e.style);
if (Number.isFinite(e.textRotation) && Math.abs(e.textRotation) > 1e-12) out.trot = normAngle(e.textRotation);
if ((t === 0 || t === 1) && Number.isFinite(e.obliqueAngle) && Math.abs(e.obliqueAngle) > 1e-9) out.obl = normAngle(e.obliqueAngle); // slanted extension lines (DIMEDIT Oblique, 0.7 D7.14)
return out;
}
Hatch boundary to polygon rings
64-bit
Turns a hatch's boundary paths (polylines with bulges, lines, arcs, ellipses, splines) into rings of points.
arc: n = max(4, ⌈32 × sweep ÷ 2π⌉) chords; ellipse: n = max(8, ⌈48 × sweep ÷ 2π⌉); p = c + r × (cos a, sin a)
Arc and ellipse edges are replaced by equal-angle chords, so the stored boundary is an inscribed polygon: at 32 chords per turn its area is 0.64 percent below a full circle's. The sample points themselves are 64-bit and lie on the curve. A ring needs at least 3 points to be kept.
dwg-import.js
lines 838 to 876
CadCraft 0.8.2
function hatchLoops(e) {
const rings = [];
for (const path of e.loops || []) {
const pts = [];
if (path.polyline) {
const vs = path.polyline.vertices || [], 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 (Math.abs(a.bulge || 0) > 1e-9) arcBetween(pts, a, b, a.bulge);
}
if (n) pts.push(vs[0].x, vs[0].y);
} else {
for (const ed of path.edges || []) {
if (ed.type === 'line') pts.push(ed.start.x, ed.start.y, ed.end.x, ed.end.y);
else if (ed.type === 'arc') {
let s = ed.startAngle, t = ed.endAngle;
if (ed.ccw === false) { s = -s; t = -t; } // a clockwise edge keeps its angles measured clockwise
let sweep = ed.ccw === false ? s - t : t - s;
while (sweep <= 0) sweep += TAU;
const dir = ed.ccw === false ? -1 : 1, n = Math.max(4, Math.ceil(32 * sweep / TAU));
for (let q = 0; q <= n; q++) { const a = s + dir * sweep * q / n; pts.push(ed.center.x + ed.radius * Math.cos(a), ed.center.y + ed.radius * Math.sin(a)); }
} else if (ed.type === 'ellipse') {
const len = Math.hypot(ed.majorAxis.x, ed.majorAxis.y) || 1, ratio = ed.ratio || 1, rot = Math.atan2(ed.majorAxis.y, ed.majorAxis.x);
let sweep = ed.endAngle - ed.startAngle; while (sweep <= 0) sweep += TAU;
const n = Math.max(8, Math.ceil(48 * sweep / TAU));
for (let q = 0; q <= n; q++) {
const t = ed.startAngle + sweep * q / n, x = len * Math.cos(t), y = len * ratio * Math.sin(t);
pts.push(ed.center.x + x * Math.cos(rot) - y * Math.sin(rot), ed.center.y + x * Math.sin(rot) + y * Math.cos(rot));
}
} else if (ed.type === 'spline') {
for (const p of splinePoints({ fit: ed.fit, control: ed.control, knots: ed.knots, degree: ed.degree, weights: ed.weights })) pts.push(p.x, p.y);
}
}
}
if (pts.length >= 6) rings.push(pts);
}
return rings;
}
Bulge arc to points
64-bit
The points along a polyline arc segment given by its two ends and its bulge.
θ = 4 × atan(b); r = c ÷ (2 × sin(θ ÷ 2)); center = midpoint + normal × r × cos(θ ÷ 2); n = max(2, ⌈16 × |θ| ÷ π⌉)
Closed form from the chord length c and the bulge; the chords span at most 11.25 degrees. A zero chord returns nothing. The division by sin(θ ÷ 2) is not guarded: it only nears zero for a bulge so large that the arc is almost a full circle.
dwg-import.js
lines 878 to 885
CadCraft 0.8.2
function arcBetween(pts, p, q, b) {
const theta = 4 * Math.atan(b), c = Math.hypot(q.x - p.x, q.y - p.y);
if (!c) return;
const r = c / (2 * Math.sin(theta / 2)), mx = (p.x + q.x) / 2, my = (p.y + q.y) / 2, h = r * Math.cos(theta / 2);
const ux = -(q.y - p.y) / c, uy = (q.x - p.x) / c, cx = mx + ux * h, cy = my + uy * h;
const a0 = Math.atan2(p.y - cy, p.x - cx), n = Math.max(2, Math.ceil(16 * Math.abs(theta) / Math.PI));
for (let k = 1; k < n; k++) { const t = a0 + theta * k / n; pts.push(cx + Math.abs(r) * Math.cos(t), cy + Math.abs(r) * Math.sin(t)); }
}
Spline edge to points
64-bit
Points along a spline edge of a hatch boundary.
n = min(400, max(16, 8 × control points)); de Boor: d_j = (1 − a) × d_{j−1} + a × d_j, a = (u − k_i) ÷ (k_{i+p−r+1} − k_i); point = (x, y) ÷ w
Evaluated at n + 1 equally spaced parameters with weights carried as a third coordinate. The last parameter is pulled in by 1e-9 × the knot range so it stays inside the last span. A zero knot difference uses a = 0. When the knot count does not fit the control points, the fit points or the control points are used as the outline instead.
dwg-import.js
lines 812 to 836
CadCraft 0.8.2
function splinePoints(e) {
// a fit-point spline with a tolerance: the reader's curve through the fit points (approx*), for drawing
if (!(e.control && e.control.length) && e.approxControl && e.approxKnots) e = { ...e, control: e.approxControl, knots: e.approxKnots, weights: null };
const cp = e.control || [], k = e.knots || [], p = e.degree || 3;
// 0.6.1: the curve itself when its control points and knots are known (the reader computes them for a fit-point
// spline when it can) — the fit points alone are only a coarse outline of it; they are the fallback
if (!(cp.length >= 2 && k.length === cp.length + p + 1) && e.fit && e.fit.length > 1) return e.fit;
if (cp.length < 2) return [];
if (k.length !== cp.length + p + 1) return cp;
const w = e.weights && e.weights.length === cp.length ? e.weights : null;
const lo = k[p], hi = k[k.length - p - 1], n = Math.min(400, Math.max(16, cp.length * 8)), out = [];
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], 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]];
}
out.push({ x: d[p][0] / d[p][2], y: d[p][1] / d[p][2] });
}
return out;
}
Picture and wipeout frame
64-bit
The corners of an image's frame or clip boundary in drawing coordinates.
W(q) = p + (q_x + 0.5) × u + (h − 0.5 − q_y) × v
An affine map from pixel coordinates (y down) with the half-pixel offsets of the DXF reference: the insertion point is the lower-left corner, u and v are one pixel along and up. Sums of products in 64-bit; 0.5 is exact.
Each offset is index × spacing, a product and not a running sum, so the last copy is as exact as the first. Cosine and sine of the rotation are taken once.
dwg-import.js
lines 569 to 574
CadCraft 0.8.2
const cols = Math.max(1, e.cols || 1), rows = Math.max(1, e.rows || 1);
const c = Math.cos(ins.rot), s = Math.sin(ins.rot);
for (let r = 0; r < rows; r++) for (let k = 0; k < cols; k++) {
const dx = k * (e.colSpacing || 0), dy = r * (e.rowSpacing || 0);
out.push({ ...ins, x: ins.x + dx * c - dy * s, y: ins.y + dx * s + dy * c });
}
Viewport scale
64-bit
The scale of a sheet viewport and the model point at its center.
s = viewport height ÷ view height (1 when the view height is not above 0)
One 64-bit division. The scale is stored as this quotient, not as a named ratio such as 1:50.
dwg-import.js
lines 611 to 613
CadCraft 0.8.2
const s = e.viewHeight > 0 ? e.height / e.viewHeight : 1;
const mc = vpModelCentre(dc, t, Number.isFinite(e.twist) ? e.twist : 0); // the view centre is turned by the twist (DCS)
const vp = { type: 'viewport', ...base, x: c.x, y: c.y, w: e.width, h: e.height, cx: mc.x, cy: mc.y, s };
Hatch pattern lines
64-bit
The file's own pattern lines: angle in degrees, base point, and offset along and across each line.
angle° = angle × 180 ÷ π; dx = o_x cos a + o_y sin a; dy = −o_x sin a + o_y cos a
The offset is rotated from world axes into the line's own frame with the angle in radians as read. The angle is also stored in degrees, a multiplication by 180 and a division by π. A hatch with more than 64 line families keeps none of them, and each family keeps at most 32 dashes.
dwg-import.js
lines 557 to 561
CadCraft 0.8.2
const pl = (e.patternLines || []).filter((l) => [l.angle, l.base?.x, l.base?.y, l.offset?.x, l.offset?.y].every(Number.isFinite));
if (pl.length && pl.length <= 64) h.patlines = pl.map((l) => {
const ca = Math.cos(l.angle), sa = Math.sin(l.angle);
return { angle: l.angle * 180 / Math.PI, x: l.base.x, y: l.base.y, dx: l.offset.x * ca + l.offset.y * sa, dy: -l.offset.x * sa + l.offset.y * ca, dashes: (l.dashes || []).filter(Number.isFinite).slice(0, 32) };
});
Lineweight in millimeters
64-bit
Converts a lineweight from hundredths of a millimeter to millimeters.
mm = v ÷ 100 for v ≥ 0; −2 and −3 kept as codes
The file holds a whole number of hundredths, so the format sets the resolution at 0.01 mm. The division gives the nearest 64-bit number to that decimal.
dwg-import.js
line 92
CadCraft 0.8.2
const lwMm = (v) => (v === -2 || v === -3 ? v : Number.isFinite(v) && v >= 0 ? v / 100 : undefined); // 1/100 mm → mm; -1 by layer = absent
Multi-line text rotation and oblique angle
64-bit
The rotation of multi-line text from its direction vector, and its oblique angle from the text's format code.
rot = atan2(xAxis_y, xAxis_x); oblique = degrees in the \Q code × π ÷ 180
The rotation is exact to atan2. The oblique angle is parsed from decimal text inside the string, so it carries only the digits written there; this app writes it with 6 decimals of a degree.
Sizes are copied as 64-bit numbers. The number of decimal places is rounded to a whole number and capped at 8, so a file asking for more places shows 8.
dwg-import.js
lines 105 to 111
CadCraft 0.8.2
const styleFrom = (src, base) => {
const st = stFromVars(src, { ...base }); // every variable CadCraft has a key for (dwg-props.js)
if (!(st.scale > 0)) st.scale = 1;
for (const k of ['dec', 'adec', 'tad', 'zin']) st[k] = Math.max(0, Math.min(8, Math.round(st[k] || 0)));
lunitOk(st);
return st;
};
Which viewport is the sheet
64-bit
Picks the viewport that stands for the sheet itself: at scale 1 and covering every other viewport.
The cover test is scaled to the viewport's size (at least 1 unit); the scale test is absolute. The result decides which viewport is dropped, not any stored number.
dwg-import.js
lines 887 to 903
CadCraft 0.8.2
/**
* The viewport that is the sheet itself, among a layout's converted paper-space entities (D7.18), or undefined.
* CadCraft's own record (xdata) wins; else it is the viewport that shows paper space 1:1 and covers every other viewport
* (the largest such); else, in a layout with several, the first one when it covers all the others. A lone viewport at
* another scale is a real one and stays. (The old rule took the first viewport found, which hid a real one listed first.)
*/
export function sheetViewport(list) {
const vps = list.filter((e) => e.type === 'viewport');
const own = vps.find((v) => v.cc);
if (own) return own;
const tol = (v) => 1e-6 * Math.max(1, v.w, v.h);
const covers = (v, o) => Math.abs(o.x - v.x) + o.w / 2 <= v.w / 2 + tol(v) && Math.abs(o.y - v.y) + o.h / 2 <= v.h / 2 + tol(v);
const all = (v) => vps.every((o) => o === v || covers(v, o));
const unit = vps.filter((v) => Math.abs(v.s - 1) <= 1e-6 && all(v)).sort((a, b) => b.w * b.h - a.w * a.h);
if (unit.length) return unit[0];
return vps.length > 1 && all(vps[0]) ? vps[0] : undefined;
}
In detail
Method
Direct copy of doubles; closed-form arc from a bulge (θ = 4 atan b); de Boor evaluation of spline edges; pixel-to-world affine map for picture frames.
Number types
64-bit numbers throughout. Lineweights arrive as whole hundredths of a millimeter and are divided by 100.
Tolerances
Dimension measurement 1e-6 × max(1, measurement); zero lengths 1e-12 (absolute); a bulge counts as an arc above 1e-9; the sheet's own viewport is recognized within 1e-6 × max(1, width, height) and a scale within 1e-6 of 1.
Limits
Hatch boundaries with arcs, ellipses or splines become polygons, so a hatch's outline and area are those of the polygon. A full circle becomes 32 chords, an inscribed polygon whose area is 0.64 percent below the circle's (16 × sin(π ÷ 16) ÷ π = 0.9936).
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.