Turns the lines and blocks of a DWG or DXF drawing into wall center lines, thicknesses, door and window positions, and a reference underlay.
How the precision is kept
Drawing coordinates are multiplied by a unit factor to millimeters and processed in 64-bit numbers. Pairing lines into walls, joining chains and closing corners depend on fixed tolerances in millimeters, so the result follows the drawing only within those tolerances. A traced thickness that differs from the chosen type is rounded to a whole millimeter.
Open item. Keep the traced wall thickness as measured instead of rounding it to a whole millimeter before the wall type is chosen.
Calculations under this policy
10 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.
All six factors are exact by definition. drawingParts uses the caller's unit when one is given, else this table, else 1; a drawing with any other INSUNITS code is therefore read as millimeters.
The bounding box of a block's lines, polylines, circles, arcs and nested blocks.
arc: its two ends and every quarter point k × π ÷ 2 inside the sweep
An arc contributes its end points and the quarter points it passes, found by stepping k from ceil(a₀ ÷ (π ÷ 2)). Nested block references add the four corners of the inner box after rotation and scale, to a depth of 4. Polyline bulges are not followed, so a bulged segment adds only its vertices.
model-trace.js
lines 57 to 76
BimCraft 0.8.1
function extents(name, depth = 0) {
if (extCache.has(name)) return extCache.get(name);
const b = blocks.get(name), e = { x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity };
const g = (x, y) => { if (x < e.x0) e.x0 = x; if (x > e.x1) e.x1 = x; if (y < e.y0) e.y0 = y; if (y > e.y1) e.y1 = y; };
for (const en of b?.entities || []) {
if (en.type === 'LINE') { g(en.start.x, en.start.y); g(en.end.x, en.end.y); }
else if (en.type === 'LWPOLYLINE' || en.type === 'POLYLINE_2D') for (const v of en.vertices || []) g(v.x, v.y);
else if (en.type === 'CIRCLE') { g(en.center.x - en.radius, en.center.y - en.radius); g(en.center.x + en.radius, en.center.y + en.radius); }
else if (en.type === 'ARC') { // its ends and the quarter points it passes (angles in radians, anticlockwise)
const a0 = en.startAngle || 0, a1 = en.endAngle ?? Math.PI * 2, sweep = ((a1 - a0) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) || 2 * Math.PI;
const at = (a) => g(en.center.x + en.radius * Math.cos(a), en.center.y + en.radius * Math.sin(a));
at(a0); at(a0 + sweep);
for (let k = Math.ceil(a0 / (Math.PI / 2)); k * Math.PI / 2 < a0 + sweep; k++) at(k * Math.PI / 2);
}
else if (en.type === 'INSERT' && depth < 4) { const s = extents(en.block, depth + 1); if (s) for (const [x, y] of [[s.x0, s.y0], [s.x1, s.y1], [s.x0, s.y1], [s.x1, s.y0]]) { const c = Math.cos(en.rotation || 0), sn = Math.sin(en.rotation || 0), sx = en.scale?.x ?? 1, sy = en.scale?.y ?? 1; g(en.insert.x + c * x * sx - sn * y * sy, en.insert.y + sn * x * sx + c * y * sy); } }
}
const out = Number.isFinite(e.x0) ? e : null;
extCache.set(name, out);
return out;
}
Box of a placed block
64-bit
The box of a block reference in plan millimeters.
corner = (insert + R(rotation) · (scale · block corner)) × unit; box = min and max of the four corners
Each corner of the block's extents is scaled, rotated with one cosine and sine of the reference's rotation, moved to the insertion point and multiplied by the unit factor. The stored rotation is converted to degrees with × 180 ÷ π.
model-trace.js
lines 86 to 92
BimCraft 0.8.1
const ex = extents(e.block), c = Math.cos(e.rotation || 0), s = Math.sin(e.rotation || 0), sx = e.scale?.x ?? 1, sy = e.scale?.y ?? 1;
let box = null;
if (ex) {
const pts = [[ex.x0, ex.y0], [ex.x1, ex.y0], [ex.x1, ex.y1], [ex.x0, ex.y1]].map(([x, y]) => [(e.insert.x + c * x * sx - s * y * sy) * um, (e.insert.y + s * x * sx + c * y * sy) * um]);
box = [Math.min(...pts.map((q) => q[0])), Math.min(...pts.map((q) => q[1])), Math.max(...pts.map((q) => q[0])), Math.max(...pts.map((q) => q[1]))];
}
inserts.push({ block: e.block, layer, x: e.insert.x * um, y: e.insert.y * um, rot: (e.rotation || 0) * 180 / Math.PI, box });
Joining lines into chains
64-bit
Links line segments that share an end point into chains.
key = round(x ÷ tol), round(y ÷ tol)
Ends are matched by rounding each coordinate to the nearest multiple of tol (1 mm from wallsFromLines). Two ends closer than tol that fall on either side of a rounding boundary get different keys and are not joined. The chain keeps the original coordinates, not the rounded ones.
model-trace.js
lines 104 to 128
BimCraft 0.8.1
/** Chains of segments joined end to end (within tol): [[p0, p1, …]] (closed ones repeat their first point). */
function chainsOf(segs, tol) {
const key = (p) => Math.round(p[0] / tol) + ',' + Math.round(p[1] / tol);
const at = new Map(), used = new Array(segs.length).fill(false);
segs.forEach((s, i) => { for (const e of [0, 1]) { const k = key(e ? [s[2], s[3]] : [s[0], s[1]]); if (!at.has(k)) at.set(k, []); at.get(k).push(i); } });
const out = [];
for (let i = 0; i < segs.length; i++) {
if (used[i]) continue;
used[i] = true;
let pts = [[segs[i][0], segs[i][1]], [segs[i][2], segs[i][3]]];
for (const dir of [1, -1]) {
for (;;) {
const end = dir === 1 ? pts[pts.length - 1] : pts[0];
const cand = (at.get(key(end)) || []).find((j) => !used[j]);
if (cand === undefined) break;
used[cand] = true;
const s = segs[cand], a = [s[0], s[1]], b = [s[2], s[3]];
const next = len(a, end) <= len(b, end) ? b : a;
if (dir === 1) pts.push(next); else pts.unshift(next);
}
}
out.push(pts);
}
return out;
}
Chain offset to the wall's center line
64-bit
Moves a chain of face lines sideways by half the wall thickness, with mitred corners.
n = (−Δy, Δx) ÷ L × d; corner = axisMeet of neighboring offset lines
Each segment is moved along its unit normal; neighboring offset lines are intersected with axisMeet from model-trim.js, and a corner where they are within about 0.06° of parallel keeps the unmitred end. A chain is closed when its ends are within 1e-6 mm.
model-trace.js
lines 129 to 148
BimCraft 0.8.1
/** A chain offset by d to its left (mitred corners; a closed chain stays closed). */
function offsetChain(pts, d) {
const closed = pts.length > 2 && len(pts[0], pts[pts.length - 1]) < 1e-6;
const P = closed ? pts.slice(0, -1) : pts, n = P.length;
const lines = [];
for (let i = 0; i < (closed ? n : n - 1); i++) {
const a = P[i], b = P[(i + 1) % n], L = len(a, b) || 1, nx = -(b[1] - a[1]) / L * d, ny = (b[0] - a[0]) / L * d;
lines.push([[a[0] + nx, a[1] + ny], [b[0] + nx, b[1] + ny]]);
}
const out = [];
for (let i = 0; i < lines.length; i++) {
const cur = lines[i];
let start = cur[0], end = cur[1];
const prev = i > 0 ? lines[i - 1] : closed ? lines[lines.length - 1] : null, next = i < lines.length - 1 ? lines[i + 1] : closed ? lines[0] : null;
if (prev) { const m = axisMeet(prev[0], prev[1], cur[0], cur[1]); if (m) start = m.p; }
if (next) { const m = axisMeet(cur[0], cur[1], next[0], next[1]); if (m) end = m.p; }
out.push([start, end]);
}
return out;
}
Walls from pairs of parallel lines
64-bit
Pairs two parallel lines as the faces of a wall and gives its center line and thickness.
d = (q.a − p.a)·n; overlap = [max(0, min(s₀, s₁)), min(L, max(s₀, s₁))]; center line = p + (d ÷ 2)·n over the overlap; t = |d|
Lines pair when the cross product of their unit directions is at most 0.02 in size. The distance d is measured from the second line's start point only, and the center line takes the first line's direction, so for lines that are not exactly parallel the thickness and direction are those of one end and one line.
model-trace.js
lines 158 to 176
BimCraft 0.8.1
const info = S.map((s) => { const a = [s[0], s[1]], b = [s[2], s[3]], L = len(a, b); return { a, b, L, u: [(b[0] - a[0]) / L, (b[1] - a[1]) / L] }; });
const cand = [];
for (let i = 0; i < info.length; i++) for (let j = i + 1; j < info.length; j++) {
const p = info[i], q = info[j];
if (Math.abs(p.u[0] * q.u[1] - p.u[1] * q.u[0]) > 0.02) continue; // about 1°
const n = [-p.u[1], p.u[0]], d = (q.a[0] - p.a[0]) * n[0] + (q.a[1] - p.a[1]) * n[1];
if (Math.abs(d) < minT || Math.abs(d) > maxT) continue;
const s0 = (q.a[0] - p.a[0]) * p.u[0] + (q.a[1] - p.a[1]) * p.u[1], s1 = (q.b[0] - p.a[0]) * p.u[0] + (q.b[1] - p.a[1]) * p.u[1];
const lo = Math.max(0, Math.min(s0, s1)), hi = Math.min(p.L, Math.max(s0, s1));
if (hi - lo < 0.5 * Math.min(p.L, q.L) || hi - lo < minLen) continue;
cand.push({ i, j, d, lo, hi, score: hi - lo });
}
cand.sort((x, y) => y.score - x.score);
const used = new Set();
for (const c of cand) {
if (used.has(c.i) || used.has(c.j)) continue;
used.add(c.i); used.add(c.j);
const p = info[c.i], n = [-p.u[1], p.u[0]], h = c.d / 2;
walls.push({ a: [p.a[0] + p.u[0] * c.lo + n[0] * h, p.a[1] + p.u[1] * c.lo + n[1] * h], b: [p.a[0] + p.u[0] * c.hi + n[0] * h, p.a[1] + p.u[1] * c.hi + n[1] * h], t: Math.abs(c.d) });
Closing traced corners
64-bit
Runs a wall end that stops short of another wall to that wall's center line.
gap = |end − meeting point| ≤ 0.75 × max(t₁, t₂) + 1 mm
The meeting point comes from axisMeet. The end moves only when the gap is above 0.01 mm, and the nearest candidate wins. Walls are processed in order and each move changes the wall in place, so later walls are tested against ends already moved.
model-trace.js
lines 182 to 196
BimCraft 0.8.1
for (const w of walls) for (const end of ['a', 'b']) {
const p = w[end], other = end === 'a' ? w.b : w.a;
let best = null;
for (const o of walls) {
if (o === w) continue;
const m = axisMeet(other, p, o.a, o.b);
if (!m) continue;
const L = len(o.a, o.b), slack = Math.max(w.t, o.t) / L;
if (m.u < -slack || m.u > 1 + slack) continue;
const gap = len(p, m.p), along = (m.t - 1) * len(other, p); // + past the end, − back inside
if (gap > Math.max(w.t, o.t) * 0.75 + 1 || along < -Math.max(w.t, o.t) - 1) continue;
if (!best || gap < best.gap) best = { gap, p: m.p };
}
if (best && best.gap > 0.01) w[end] = best.p;
}
Doors and windows from blocks
64-bit
Places a door or window on the wall nearest a block's box, with its offset and width along the wall.
s = (c − a)·u; width = |Δx·uₓ| + |Δy·u_y| of the box, between 400 mm and L − 50 mm; offset = s − width ÷ 2
The box center is projected on each wall with dot products; the wall with the smallest distance across wins. A door's width is capped at 1.6 × its type width. The width is the box's extent along the wall, which for a box not aligned with the wall is larger than the block itself.
model-trace.js
lines 203 to 228
BimCraft 0.8.1
export function openingsFromInserts(inserts, map, walls) {
const out = [];
for (const ins of inserts) {
const m = map[ins.block];
if (!m || !m.typeKey || !ins.box) continue;
const type = openingType(m.typeKey);
if (!type) continue;
const c = [(ins.box[0] + ins.box[2]) / 2, (ins.box[1] + ins.box[3]) / 2];
let best = null;
walls.forEach((w, i) => {
const L = len(w.a, w.b), u = [(w.b[0] - w.a[0]) / L, (w.b[1] - w.a[1]) / L], n = [-u[1], u[0]];
const s = (c[0] - w.a[0]) * u[0] + (c[1] - w.a[1]) * u[1], d = (c[0] - w.a[0]) * n[0] + (c[1] - w.a[1]) * n[1];
if (s < 0 || s > L) return;
if (Math.abs(d) > w.t / 2 + 300 + (type.kind === 'door' ? type.width : 0)) return;
const along = Math.abs((ins.box[2] - ins.box[0]) * u[0]) + Math.abs((ins.box[3] - ins.box[1]) * u[1]);
// a door's box holds its swing: the opening is the box's extent along the wall, less a frame's worth; a window's as it is
const width = Math.max(400, Math.min(L - 50, type.kind === 'door' ? Math.min(along, type.width * 1.6) : along));
const sc = Math.abs(d);
if (!best || sc < best.sc) best = { sc, i, s, width, d, kind: type.kind };
});
if (!best) continue;
// a door's box holds its swing, so it lies to the side the door opens to: that side is its face
out.push({ wall: best.i, typeKey: m.typeKey, kind: best.kind, offset: best.s - best.width / 2, width: best.width, face: best.d >= 0 ? 'left' : 'right', block: ins.block });
}
return out;
}
Wall type for a traced thickness
64-bit
Chooses the wall type for a traced wall from its measured thickness.
|t − t₀| > 1 mm → type at Math.round(t) mm, else the chosen type
A thickness within 1 mm of the chosen type's uses that type's thickness. Otherwise the measured thickness is rounded to a whole millimeter with Math.round before the type is looked up, so the wall is stored at the rounded thickness.
Unit factor from the drawing's INSUNITS; chains joined on a rounding grid; mitred offset; parallel pairs matched by distance and overlap; wall ends extended to the nearest wall by line intersection; block boxes projected onto the nearest wall.
Number types
64-bit numbers. Unit factors 25.4, 304.8, 10, 1000 and 914.4 are exact.
Tolerances
Chain joining grid 1 mm. Pairs: |sin angle| ≤ 0.02, distance 50 mm to 600 mm by default, overlap at least half the shorter line and at least 100 mm. Corner closing: gap up to 0.75 × the larger thickness + 1 mm; moved only when the gap is above 0.01 mm. Duplicate walls: both ends within 1 mm. Lines shorter than 1 mm and walls shorter than 100 mm are dropped. Bulge above 1e-9 counts as curved.
Limits
Arcs, circles and bulged polyline segments are counted but make no walls. INSUNITS codes other than 1, 2, 4, 5, 6 and 10 are read as millimeters unless the caller gives a unit. Lines up to about 1° from parallel are paired, and the wall then takes the first line's direction.
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.