The edges and faces made when a DXF or DWG drawing is brought into the 3D model as a site plan: unit scaling, block placement, and arcs, circles, ellipses and splines turned into straight segments.
How the precision is kept
Coordinates are read as 64-bit numbers, transformed by the block placements in the drawing's own unit, and multiplied once by the unit factor to meters. Curves are not kept as curves: arcs, circles and ellipses become 48 straight segments per full turn and splines 12 per knot span, so the sampling sets how closely the result follows the drawing. Line work is laid flat at z = 0; 3D faces keep their heights. The resulting points are then welded on a 1e-5 m grid (see Welding of imported geometry).
Open item. Choose the number of segments from the radius and a chord tolerance instead of a fixed 48 per turn.
Calculations under this policy
15 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.
The factor from the drawing's unit ($INSUNITS) to meters.
in 0.0254; ft 0.3048; mi 1609.344; mm 0.001; cm 0.01; m 1; km 1000; yd 0.9144
Each factor is the exact defined value of the unit in meters, written as a decimal literal. A unit code that is not in this table has no entry, and the drawing is then read as if it stated no unit.
The drawing's own unit code is used when it is in the table. Otherwise the unit the person chose is used, and if that is missing the drawing is read as meters (factor 1).
m3d/io.js
lines 969 to 970
CadCraft 0.8.2
let units = null, k = UNIT_M[opts.unit] || 1;
if (DXF_UNITS[plan.insUnits]) { units = DXF_UNITS[plan.insUnits][0]; k = DXF_UNITS[plan.insUnits][1]; }
Mirrored entities
64-bit
How an entity drawn in a flipped coordinate system is read.
x → −x when the extrusion direction's z is negative
Only the sign of group 230 (the z of the extrusion direction) is read; when it is negative, x is negated. An extrusion direction that is tilted rather than straight up or down is not applied.
m3d/io.js
lines 608 to 609
CadCraft 0.8.2
const f = num(g, 230, 1) < 0;
const X = (x) => (f ? -x : x);
Moves each line segment by the block placements above it and converts it to meters.
x′ = a·x + c·y + e; y′ = b·x + d·y + f; then × k
The 2D affine is applied in the drawing's unit and the result multiplied by the unit factor, one multiplication per coordinate. z is set to 0. A segment whose two ends come out equal is dropped, and a coordinate of 1e7 m or more stops the import.
m3d/io.js
lines 976 to 986
CadCraft 0.8.2
// 2D affine [a, b, c, d, e, f]: x' = a x + c y + e, y' = b x + d y + f
const T = (m, p) => [m[0] * p[0] + m[2] * p[1] + m[4], m[1] * p[0] + m[3] * p[1] + m[5]];
const Tmul = (m, n) => [m[0] * n[0] + m[2] * n[1], m[1] * n[0] + m[3] * n[1], m[0] * n[2] + m[2] * n[3], m[1] * n[2] + m[3] * n[3], m[0] * n[4] + m[2] * n[5] + m[4], m[1] * n[4] + m[3] * n[5] + m[5]];
const seg = (m, p, q) => {
const a = T(m, p), b = T(m, q);
if (a[0] === b[0] && a[1] === b[1]) return;
if (!(Number.isFinite(a[0]) && Number.isFinite(a[1]) && Number.isFinite(b[0]) && Number.isFinite(b[1]))) return;
if (Math.max(Math.abs(a[0]), Math.abs(a[1]), Math.abs(b[0]), Math.abs(b[1])) * k >= 1e7) throw new Error(t('m3d.io.this_drawing_reaches_more_than_10'));
if (segs.length >= MAX_SEGS) throw new Error(t('m3d.io.this_drawing_has_more_than_2'));
segs.push([[a[0] * k, a[1] * k, 0], [b[0] * k, b[1] * k, 0]]);
};
Arcs, circles and bulges as segments
64-bit
The points that stand for an arc, a circle or a polyline bulge.
n = max(2, ⌈|sweep| ÷ (2π ÷ 48)⌉); pⱼ = c + r (cos aⱼ, sin aⱼ), aⱼ = a₀ + sweep × j ÷ n; bulge: sweep = 4 atan b
Each point's angle is computed from j directly. With 48 segments per turn each chord's middle is r × (1 − cos(π ÷ 48)), about 0.21 % of the radius, inside the true arc. For a bulge the center is found from the chord and (1 ÷ b − b) ÷ 2; a very small bulge puts the center far away, and each point is then a large center coordinate plus a large, nearly opposite radius term.
m3d/io.js
lines 987 to 999
CadCraft 0.8.2
const arcPts = (c, r, a0, sweep) => {
const n = Math.max(2, Math.ceil(Math.abs(sweep) / (2 * Math.PI / CIRCLE_SEGS)));
const out = [];
for (let j = 0; j <= n; j++) { const a = a0 + (sweep * j) / n; out.push([c[0] + r * Math.cos(a), c[1] + r * Math.sin(a)]); }
return out;
};
const bulgePts = (p, q, b) => {
if (!b || Math.abs(b) < 1e-12) return [p, q];
const s = (1 / b - b) / 2;
const c = [(p[0] + q[0]) / 2 - s * (q[1] - p[1]) / 2, (p[1] + q[1]) / 2 + s * (q[0] - p[0]) / 2];
const r = Math.hypot(p[0] - c[0], p[1] - c[1]);
return arcPts(c, r, Math.atan2(p[1] - c[1], p[0] - c[0]), 4 * Math.atan(b));
};
Ellipse as segments
64-bit
The points that stand for an ellipse or an elliptical arc.
p = c + major × cos t + minor × sin t, minor = major turned 90° × ratio
Sampled at equal steps of the ellipse parameter, 48 per full turn and at least 4, each t computed from its index. The curve is full when the sweep is within 1e-9 of 2π.
m3d/io.js
lines 847 to 858
CadCraft 0.8.2
/** An ellipse (WCS centre, major axis, ratio, parameters) as a sampled curve; flip = extrusion facing down. */
function ellipseCurve(c, maj, ratio, t0, t1, flip) {
if (!(Math.hypot(maj[0], maj[1]) > 0) || !(ratio > 0)) return NONE;
const mn = flip ? [maj[1] * ratio, -maj[0] * ratio] : [-maj[1] * ratio, maj[0] * ratio];
let sw = t1 - t0;
while (sw <= 0) sw += 2 * Math.PI;
const full = Math.abs(sw - 2 * Math.PI) < 1e-9;
const n = Math.max(4, Math.ceil(sw / (2 * Math.PI / CIRCLE_SEGS)));
const pts = [];
for (let k = 0; k <= (full ? n - 1 : n); k++) { const t = t0 + (sw * k) / n; pts.push([c[0] + maj[0] * Math.cos(t) + mn[0] * Math.sin(t), c[1] + maj[1] * Math.cos(t) + mn[1] * Math.sin(t)]); }
return { t: 'curve', kind: 'ellipse', pts, closed: full };
}
Spline (NURBS) as segments
64-bit
The points that stand for a spline with control points.
de Boor on homogeneous points (x·w, y·w, w); 12 samples per knot span
Evaluated with the de Boor recurrence in 64-bit; a zero knot interval gives a blend factor of 0 instead of a division. The last sample is taken at the end knot minus 1e-12 of the knot range, not at the end knot itself.
m3d/io.js
lines 891 to 919
CadCraft 0.8.2
function nurbsPts(p, U, P, W) {
const n = P.length;
if (!(p >= 1) || n < p + 1 || U.length !== n + p + 1 || P.some((q) => !q.every(Number.isFinite)) || !U.every(Number.isFinite)) return null;
const a = U[p], b = U[n];
if (!(b > a)) return null;
const point = (u) => {
let k = p;
while (k < n - 1 && u >= U[k + 1]) k++;
const d = [];
for (let j = 0; j <= p; j++) { const q = P[k - p + j], w = W ? W[k - p + j] || 1 : 1; d.push([q[0] * w, q[1] * w, w]); }
for (let r = 1; r <= p; r++) {
for (let j = p; j >= r; j--) {
const i = k - p + j, den = U[i + p - r + 1] - U[i];
const al = den > 0 ? (u - U[i]) / den : 0;
d[j] = [0, 1, 2].map((m) => (1 - al) * d[j - 1][m] + al * d[j][m]);
}
}
const w = d[p][2] || 1;
return [d[p][0] / w, d[p][1] / w];
};
const out = [];
for (let k = p; k < n; k++) {
const u0 = U[k], u1 = U[k + 1];
if (!(u1 > u0)) continue;
for (let j = 0; j < SPLINE_PER_SPAN; j++) out.push(point(u0 + ((u1 - u0) * j) / SPLINE_PER_SPAN));
}
out.push(point(b - (b - a) * 1e-12));
return out.length >= 2 && out.length < 200000 ? out : null;
}
Spline stored with fit points only
64-bit
The points that stand for a spline that carries fit points and no control points.
centripetal Catmull–Rom, 12 samples per span
The original curve is not rebuilt: a centripetal Catmull–Rom curve through the fit points is used, and the code's own comment says it is close to, not the same as, the source program's fit. Knot steps are the square root of the point spacing with a floor of 1e-12.
m3d/io.js
lines 866 to 890
CadCraft 0.8.2
/**
* A smooth curve through fit points (a spline stored with fit points only, as some writers save it): centripetal
* Catmull–Rom, SPLINE_PER_SPAN points per span. Close to, not the same as, the program's own fit.
*/
function fitCurve(F, closed) {
const n = F.length;
if (n < 3) return F.slice();
const at = (i) => (closed ? F[(i + n) % n] : i < 0 ? [2 * F[0][0] - F[1][0], 2 * F[0][1] - F[1][1]] : i >= n ? [2 * F[n - 1][0] - F[n - 2][0], 2 * F[n - 1][1] - F[n - 2][1]] : F[i]);
const out = [];
const spans = closed ? n : n - 1;
for (let i = 0; i < spans; i++) {
const p0 = at(i - 1), p1 = at(i), p2 = at(i + 1), p3 = at(i + 2);
const tj = (a, b) => Math.max(1e-12, Math.sqrt(Math.hypot(b[0] - a[0], b[1] - a[1])));
const t1 = tj(p0, p1), t2 = t1 + tj(p1, p2), t3 = t2 + tj(p2, p3);
for (let k = 0; k < SPLINE_PER_SPAN; k++) {
const t = t1 + ((t2 - t1) * k) / SPLINE_PER_SPAN;
const L = (a, b, ta, tb) => [0, 1].map((m) => ((tb - t) * a[m] + (t - ta) * b[m]) / (tb - ta));
const A1 = L(p0, p1, 0, t1), A2 = L(p1, p2, t1, t2), A3 = L(p2, p3, t2, t3);
const B1 = L(A1, A2, 0, t2), B2 = L(A2, A3, t1, t3);
out.push(L(B1, B2, t1, t2));
}
}
if (!closed) out.push(F[n - 1]);
return out;
}
Bulge arcs in hatch boundaries
64-bit
The points of a curved side of a hatch boundary given as a polyline bulge.
center = chord midpoint + ((1 ÷ b − b) ÷ 2) × (chord turned 90°) ÷ 2; sweep = 4 atan b
The same bulge formula and the same 48 segments per turn as for polylines; a bulge below 1e-12 in size is a straight side.
cos and sin of the rotation are computed once per insert and reused. Nested inserts multiply their 2D transforms, up to 16 levels deep, so each level adds the rounding of one product.
m3d/io.js
lines 1112 to 1120
CadCraft 0.8.2
const cs = Math.cos(e.rot), sn = Math.sin(e.rot);
// translate(x,y) · rotate · scale · translate(−base)
const RS = [cs * e.sx, sn * e.sx, -sn * e.sy, cs * e.sy, e.x, e.y];
const local = Tmul(RS, [1, 0, 0, 1, -b.base[0], -b.base[1]]);
const cols = Math.max(1, e.cols || 1), rows = Math.max(1, e.rows || 1);
for (let r = 0; r < Math.min(rows, 1000); r++) for (let c = 0; c < Math.min(cols, 1000); c++) {
const off = [cs * c * e.dc - sn * r * e.dr, sn * c * e.dc + cs * r * e.dr];
draw(b.ents, Tmul(m, Tmul([1, 0, 0, 1, off[0], off[1]], local)), depth + 1);
}
Hatch holes
64-bit
Which loops of a hatch are outlines and which are holes.
area = ½ |Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)|; a loop inside an odd number of larger loops is a hole
Loops are ordered by shoelace area and tested by an even-odd crossing count of their first point against each larger loop, with no tolerance; a first point that lies on another loop's edge can fall either way.
m3d/io.js
lines 1074 to 1082
CadCraft 0.8.2
// which loops are holes: a loop inside an odd number of others
const L = e.loops.map((pts) => ({ pts, A: Math.abs(area2(pts)) })).sort((x, y) => y.A - x.A);
const outers = [];
for (const l of L) {
const inside = L.filter((o) => o !== l && o.A > l.A && inPoly(l.pts[0], o.pts));
if (inside.length % 2 === 0) outers.push({ l, holes: [] });
else { const host = inside.sort((x, y) => x.A - y.A)[0]; const o = outers.find((q) => q.l === host); if (o) o.holes.push(l); }
}
for (const o of outers) polys.push({ loops: [o.l, ...o.holes].map((q) => q.pts.map((p) => P3(m, [p[0], p[1], 0]))) });
3D face corners
64-bit
The corners of 3D faces and solids, which keep their heights.
(x, y) placed by the block transform; all three × k
x and y go through the 2D block transform and the unit factor; z is only multiplied by the unit factor, so a block's own transform does not act on heights.
2D affine transforms for block inserts; bulge arcs from the chord and bulge; de Boor evaluation of NURBS; centripetal Catmull–Rom for splines stored with fit points only; even-odd test for hatch holes.
Number types
64-bit numbers.
Tolerances
A bulge below 1e-12 is a straight segment. A full ellipse is a sweep within 1e-9 of 2π. Repeated points closer than 1e-12 (drawing units) are dropped. A coordinate of 1e7 m or more stops the import.
Limits
A 48-segment circle's chords lie up to 0.21 % of the radius inside the true circle (1 − cos(π ÷ 48)). Only the sign of an entity's extrusion direction is used. Drawing unit codes other than inches, feet, miles, millimeters, centimeters, meters, kilometers and yards are treated as not stated.
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.