Resources Numerical accuracy Calculation policy

Wall joins and plan outlines

Works out the plan outline of every drawn wall with its corners, tees and crossings cleaned, which becomes the wall's solid and closes rooms.

How the precision is kept

Wall ends within 1 mm of each other share a node. At a node the walls are sorted by direction and each neighboring pair meets where one wall's left face line crosses the next wall's right face line, solved in closed form in 64-bit millimeters. Curved walls use line and circle or circle and circle intersections. An outline that crosses itself falls back to a plain rectangle.

Calculations under this policy

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

model-walls.js BimCraft 0.8.1 64-bit 9 calculations Reviewed

Tests tools/bimcraft-test/model-test.mjs, tools/bimcraft-test/comforts-test.mjs

Where two face lines meet

64-bit

The point where two lines, each given by a point and a direction, cross.

s = ((p₂ − p₁) × d₂) ÷ (d₁ × d₂); point = p₁ + s·d₁

One division by the cross product of the two directions. Lines are treated as parallel when |d₁ × d₂| < 1e-9; the callers pass unit directions, so that is an angle of about 1e-9 radians. Just above that threshold the division amplifies rounding in the numerator.

  • model-walls.js
  • lines 102 to 108
  • BimCraft 0.8.1
/** Where p1 + s·d1 meets p2 + t·d2, or null when they are parallel. */
function meet(p1, d1, p2, d2) {
  const c = cross(d1, d2);
  if (Math.abs(c) < 1e-9) return null;
  const s = cross(sub(p2, p1), d2) / c;
  return add(p1, mul(d1, s));
}

Nodes and tees

64-bit

Groups wall ends that meet and finds ends that land on another wall's side.

An end joins the first node found within tol (1 mm, absolute) of it, so grouping depends on the order of the walls. A tee is recorded when the node lies more than tol from both ends of the other wall and within half its thickness plus tol of its center line.

  • model-walls.js
  • lines 110 to 128
  • BimCraft 0.8.1
/** The nodes where walls meet: { p, ends: [{ w, end }], tees: [{ w, s }] } (w = index into the prepared list). */
export function wallNodes(W, tol = 1) {
  const nodes = [];
  for (let i = 0; i < W.length; i++) for (const end of ['a', 'b']) {
    const p = W[i][end];
    let n = nodes.find((x) => dist(x.p, p) <= tol);
    if (!n) { n = { p, ends: [], tees: [] }; nodes.push(n); }
    n.ends.push({ w: i, end });
  }
  for (const n of nodes) {
    const here = new Set(n.ends.map((e) => e.w));
    for (let j = 0; j < W.length; j++) {
      if (here.has(j)) continue;
      const w = W[j], q = w.g.param(n.p), s = q.s, off = Math.abs(q.off);
      if (s > tol && s < w.L - tol && off <= w.h + tol) n.tees.push({ w: j, s });
    }
  }
  return nodes;
}

Corner points at a node

64-bit

The mitre or corner point between each pair of neighboring walls at a node.

c = left face of A ∩ right face of B; kept when |c − node| ≤ 4 × max(h_A, h_B) + tol

Walls are sorted by Math.atan2 of their direction. A pair is left square when the faces do not cross or when the turn is ≤ 1e-9 with the walls pointing the same way. A corner farther than four half-thicknesses plus 1 mm from the node is dropped and the ends stay square.

  • model-walls.js
  • lines 191 to 202
  • BimCraft 0.8.1
  rays.sort((x, y) => x.ang - y.ang);
  for (let i = 0; i < rays.length; i++) {
    const A = rays[i], B = rays[(i + 1) % rays.length];
    if (!A.real && !B.real) continue;   // the two halves of a T's through wall
    // the space between A (its left face) and B, the next one anticlockwise (its right face)
    const c = W[A.w].arc || W[B.w].arc ? meetFaces(A.fl, B.fr, p) : meet(add(A.o, mul(left(A.d), A.h)), A.d, add(B.o, mul(left(B.d), -B.h)), B.d);
    const turn = cross(A.d, B.d), ahead = dot(A.d, B.d);
    if (!c || (turn <= 1e-9 && ahead > 1e-9)) continue;   // parallel or overlapping: square ends (1e-9: a curved wall's tangent is not exact)
    if (dist(c, p) > 4 * Math.max(A.h, B.h) + tol) continue;   // too sharp a mitre: square
    A.left = c; B.right = c;
  }
}

Faces of curved walls

64-bit

Where a straight face meets a circular face, or two circular faces meet, nearest a given point.

line and circle: t = −b ± √(b² − c); two circles: x = (d² + r₁² − r₂²) ÷ 2d, y = √(r₁² − x²)

The line and circle case uses the quadratic formula with a unit direction; a negative discriminant returns no point, with no tolerance, so a face that just touches can be missed. In the two-circle case r₁² − x² is clamped at 0 before the square root, and circles whose centers are closer than 1e-6 mm return no point.

  • model-walls.js
  • lines 217 to 232
  • BimCraft 0.8.1
/** Where two faces meet nearest `near`: lines { p, d } and circles { c, r } (a curved wall's faces); null when they don't. */
function meetFaces(F, G, near) {
  if (F.d && G.d) return meet(F.p, F.d, G.p, G.d);
  const pick = (list) => list.filter(Boolean).sort((x, y) => dist(x, near) - dist(y, near))[0] || null;
  if (F.d || G.d) {
    const L = F.d ? F : G, K = F.d ? G : F, m = sub(L.p, K.c), bq = dot(L.d, m), cq = dot(m, m) - K.r * K.r, disc = bq * bq - cq;
    if (disc < 0) return null;
    const sq = Math.sqrt(disc);
    return pick([add(L.p, mul(L.d, -bq - sq)), add(L.p, mul(L.d, -bq + sq))]);
  }
  const dd = dist(F.c, G.c);
  if (dd < 1e-6 || dd > F.r + G.r || dd < Math.abs(F.r - G.r)) return null;   // one circle (a curve going on), or apart
  const x = (dd * dd + F.r * F.r - G.r * G.r) / (2 * dd), y = Math.sqrt(Math.max(0, F.r * F.r - x * x));
  const u = mul(sub(G.c, F.c), 1 / dd), m = add(F.c, mul(u, x));
  return pick([add(m, mul(left(u), y)), add(m, mul(left(u), -y))]);
}

Curved wall outline

64-bit

The outline of a curved wall with each arc edge kept as a bulge.

bulge = tan(sweep ÷ 4)

Each face's sweep is the difference of two Math.atan2 angles about the arc center, wrapped to the turn nearest the wall's own included angle. The outline is also flattened to chords (flatPoly in model-arcs.js) for rooms and the validity test; points closer than 1e-6 mm are merged.

  • model-walls.js
  • lines 233 to 252
  • BimCraft 0.8.1
/**
 * A curved wall's outline: its corners from the joins (A at its start, B at its end, as for a straight wall), the right face
 * from A to B and the left face back as arcs — the outline returned is in chords (rooms, the plan), its `bulged` copy keeps
 * each arc edge as a bulge on the point it starts from ([x, y, bulge], for the IFC profile).
 */
function arcOutline(w, A, B) {
  const k = w.arc, ang = (q) => Math.atan2(q[1] - k.C[1], q[0] - k.C[0]);
  const wrap = (x) => { let y = (x + Math.PI) % (2 * Math.PI); if (y <= 0) y += 2 * Math.PI; return y - Math.PI; };
  const bulge = (P, Q, want) => Math.tan((wrap(ang(Q) - ang(P) - want) + want) / 4);
  let poly = [[...A.right, bulge(A.right, B.left, k.theta)], B.left, B.centre, [...B.right, bulge(B.right, A.left, -k.theta)], A.left, A.centre].filter(Boolean);
  // repeated points: the later one keeps its bulge (the edge to it is nothing)
  poly = poly.filter((q, i) => dist(q, poly[(i + 1) % poly.length]) > 1e-6 || poly.length < 4);
  let flat = flatPoly(poly);
  if (poly.length < 4 || !simpleCCW(flat)) {
    poly = [[...w.g.at(0, -w.h), Math.tan(k.theta / 4)], w.g.at(w.L, -w.h), [...w.g.at(w.L, w.h), Math.tan(-k.theta / 4)], w.g.at(0, w.h)];
    flat = flatPoly(poly);
  }
  flat.bulged = poly;
  return flat;
}

Removing repeated and collinear points

64-bit

Drops outline points that repeat or lie on a straight run.

collinear when |ab × bc| ≤ 1e-6 × (|ab|·|bc| + 1e-12) and ab·bc ≥ 0

The repeated-point distance is 1e-6 mm, absolute. The collinear test is scaled by the two edge lengths, so it is a test on the sine of the angle (1e-6).

  • model-walls.js
  • lines 254 to 268
  • BimCraft 0.8.1
/** Drop repeated and collinear points. */
function clean(poly) {
  let p = poly.filter((q, i) => i === 0 || dist(q, poly[i - 1]) > 1e-6);
  if (p.length > 1 && dist(p[0], p[p.length - 1]) <= 1e-6) p.pop();
  let changed = true;
  while (changed && p.length > 3) {
    changed = false;
    for (let i = 0; i < p.length; i++) {
      const a = p[(i + p.length - 1) % p.length], b = p[i], c = p[(i + 1) % p.length];
      const ab = sub(b, a), bc = sub(c, b);
      if (Math.abs(cross(ab, bc)) <= 1e-6 * (Math.hypot(...ab) * Math.hypot(...bc) + 1e-12) && dot(ab, bc) >= 0) { p.splice(i, 1); changed = true; break; }
    }
  }
  return p;
}

Signed outline area

64-bit

The signed area of an outline, used for the footprint and to check its direction.

A = ½ Σ (xᵢ·yᵢ₊₁ − yᵢ·xᵢ₊₁)

The shoelace sum in point order with no compensation. With absolute plan coordinates far from the origin the terms are large and nearly cancel.

  • model-walls.js
  • line 269
  • BimCraft 0.8.1
export function polyArea(p) { let s = 0; for (let i = 0; i < p.length; i++) s += cross(p[i], p[(i + 1) % p.length]); return s / 2; }

Outline validity test

64-bit

Decides whether an outline is anticlockwise and does not cross itself.

The outline must have a signed area above 1e-6 mm². Each pair of non-neighboring edges is tested with the signs of four 64-bit cross products (segCross); only proper crossings count, so edges that touch at a point pass.

  • model-walls.js
  • lines 270 to 278
  • BimCraft 0.8.1
function simpleCCW(p) {
  if (p.length < 3 || polyArea(p) <= 1e-6) return false;
  const n = p.length;
  for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
    if (j === i + 1 || (i === 0 && j === n - 1)) continue;
    if (segCross(p[i], p[(i + 1) % n], p[j], p[(j + 1) % n])) return false;
  }
  return true;
}

Location line offset

64-bit

How far a wall's location line is from its center line for each justification.

face: ± t ÷ 2; core: ± (t ÷ 2 − layer thickness up to the core face or core middle)

Halves of the thickness and sums of layer thicknesses in 64-bit millimeters. Returns null when a core justification is asked of an assembly with no core layer.

  • model-walls.js
  • lines 291 to 302
  • BimCraft 0.8.1
/** How far the location line is from the centre line, to the wall's left (mm). */
export function locationOffset(just, t, extSide, assembly = null) {
  if (String(just).startsWith('core')) {
    const layers = assembly?.layers || [], first = layers.findIndex(l => l.core), last = layers.findLastIndex(l => l.core);
    if (first < 0) return null;
    const outer = layers.slice(0, first).reduce((s,l) => s+l.t, 0), inner = layers.slice(0, last+1).reduce((s,l) => s+l.t, 0);
    const sign = extSide === 'right' ? -1 : 1;
    return sign * (t/2 - (just === 'coreExterior' ? outer : just === 'coreInterior' ? inner : (outer+inner)/2));
  }
  const ext = extSide === 'right' ? -t / 2 : t / 2;
  return just === 'left' ? t / 2 : just === 'right' ? -t / 2 : just === 'exterior' ? ext : just === 'interior' ? -ext : 0;
}

In detail

Method
Two-line intersection by cross products; line and circle by the quadratic formula; two circles by the radical line. Outline validity by signed area and a pairwise segment crossing test.
Number types
64-bit numbers in plan millimeters.
Tolerances
Node tolerance 1 mm (absolute). Parallel test |cross| < 1e-9 on direction vectors. Mitre cut square when the corner is more than 4 half-thicknesses + 1 mm from the node. Repeated points 1e-6 mm. Collinear test 1e-6 relative to the two edge lengths. Minimum outline area 1e-6 mm². Circle centers closer than 1e-6 mm count as one circle.
Limits
The crossing test uses the sign of a 64-bit cross product, not an exact predicate, so nearly touching edges can be judged either way.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach