Resources Numerical accuracy Calculation policy

Plan view: picked points, wall outlines and dimensions

Turns a pointer position into plan millimeters, and works out the dimension chain along each wall and the wall's plan outline with its door and window openings taken out.

How the precision is kept

A pointer position becomes a plan point by one subtraction, one division by the view's scale in pixels per millimeter and one addition, in 64-bit. A wall's outline is taken into the wall's own axes with one cosine and sine, and the dimension stops are read along the chosen face. Each stop is rounded to 0.01 mm before the chain's pieces are differenced; the overall length is the unrounded difference of the face's two ends. The text itself is made by the unit formatter passed in.

Calculations under this policy

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

model-plan.js BimCraft 0.8.1 64-bit 4 calculations Reviewed

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

Dimension chain of a wall

64-bit

The overall length of a wall along one face and the chain of lengths between its ends and its openings.

overall = e₁ − e₀; piece = sᵢ₊₁ − sᵢ with s rounded to 0.01 mm

The face's ends are the smallest and largest positions of the outline corners lying within 0.5 mm of the face. Chain stops are rounded to 0.01 mm and then subtracted, so a chain piece is a difference of rounded values; the overall length is e₁ − e₀ unrounded.

  • model-plan.js
  • lines 202 to 231
  • BimCraft 0.8.1
/**
 * Dimensions for the drawn walls of a level (BM.7): each wall's length along one face — the outside face of an outside
 * wall, else its left face — and, when it holds doors or windows, a chain from the face's ends to each opening's edges
 * nearer the wall. → [{ a, b, text, offPx, offMm, key }] (a, b on the face; offPx / offMm how far out to draw it on screen
 * and on paper; the sign puts it on the face's side). fmt(mm) gives the text.
 */
export function wallDims(walls, outlines, openings, fmt, { exteriorOf = () => true } = {}) {
  const out = [];
  for (const w of walls) {
    const poly = outlines.get(w.guid);
    if (!poly) continue;
    const L = Math.hypot(w.b[0] - w.a[0], w.b[1] - w.a[1]);
    if (L < 1 || w.bulge) continue;   // a curved wall's length is in its Properties (no arc dimension yet)
    const u = [(w.b[0] - w.a[0]) / L, (w.b[1] - w.a[1]) / L], n = [-u[1], u[0]];
    const side = exteriorOf(w) ? (w.extSide === 'right' ? -1 : 1) : 1;
    const local = poly.map(([x, y]) => [(x - w.a[0]) * u[0] + (y - w.a[1]) * u[1], (x - w.a[0]) * n[0] + (y - w.a[1]) * n[1]]);
    const xs = local.filter(([, y]) => Math.abs(y - side * w.t / 2) < 0.5).map(([x]) => x);
    if (xs.length < 2) continue;
    const e0 = Math.min(...xs), e1 = Math.max(...xs);
    const P = (x) => [w.a[0] + u[0] * x + n[0] * side * w.t / 2, w.a[1] + u[1] * x + n[1] * side * w.t / 2];
    const mine = openings.filter((o) => o.wall === w.id).sort((a, b) => a.offset - b.offset);
    const stops = [e0];
    for (const o of mine) stops.push(Math.max(e0, o.offset), Math.min(e1, o.offset + o.width));
    stops.push(e1);
    const s = [...new Set(stops.map((x) => Math.round(x * 100) / 100))].sort((a, b) => a - b);
    if (mine.length) for (let i = 0; i + 1 < s.length; i++) if (s[i + 1] - s[i] > 0.5) out.push({ a: P(s[i]), b: P(s[i + 1]), text: fmt(s[i + 1] - s[i]), offPx: side * 22, offMm: side * 5, key: w.id });
    out.push({ a: P(e0), b: P(e1), text: fmt(e1 - e0), offPx: side * (mine.length ? 44 : 22), offMm: side * (mine.length ? 10 : 5), key: w.id });
  }
  return out;
}

Wall outline with openings taken out

64-bit

The pieces of a wall's plan outline left between the openings the cut plane passes through.

The outline is turned into the wall's axes (polar for a curved wall), clipped at each opening's two edges and turned back. An opening counts when the cut height lies between its sill and its head, compared without tolerance.

  • model-plan.js
  • lines 133 to 157
  • BimCraft 0.8.1
/**
 * A drawn wall's plan: its joined outline (plan points) with the openings the cut plane passes through taken out.
 * openings: [{ offset (along the wall from its start, mm, to the opening's near edge), width, sill, height }] (sill and the
 * cut height both above the level's floor).
 * → [[x, y]…][] (one polygon, or several pieces between openings)
 */
export function wallPieces(wall, poly, openings = [], cutZ = 1200) {
  const ang = Math.atan2(wall.b[1] - wall.a[1], wall.b[0] - wall.a[0]), c = Math.cos(ang), s = Math.sin(ang);
  let toL = ([x, y]) => [(x - wall.a[0]) * c + (y - wall.a[1]) * s, -(x - wall.a[0]) * s + (y - wall.a[1]) * c];
  let toW = ([u, v]) => [wall.a[0] + u * c - v * s, wall.a[1] + u * s + v * c];
  // a curved wall: along its arc and across it (polar), so openings are cut along the radius as in 3D
  if (wall.bulge) ({ toL, toW } = wallMap(wall));
  let pieces = [poly.map(toL)];
  for (const o of openings) {
    if (!(o.width > 0) || cutZ < o.sill || cutZ > o.sill + o.height) continue;   // both above the level's floor
    const next = [];
    for (const p of pieces) {
      const l = clipX(p, o.offset, -1), r = clipX(p, o.offset + o.width, 1);
      if (l.length >= 3) next.push(l);
      if (r.length >= 3) next.push(r);
    }
    pieces = next;
  }
  return pieces.map((p) => p.map(toW));
}

Clipping a polygon at a position along the wall

64-bit

Keeps the part of a polygon on one side of x = k.

t = (k − a_x) ÷ (b_x − a_x); y = a_y + t (b_y − a_y)

Points within 1e-9 of the line count as inside. The crossing point divides by the edge's extent along x, which is not zero when its two ends are on opposite sides.

  • model-plan.js
  • lines 158 to 169
  • BimCraft 0.8.1
/** Keep the part of a polygon with x ≤ k (side −1) or x ≥ k (side 1): Sutherland–Hodgman against one line. */
function clipX(p, k, side) {
  const inside = (q) => (side < 0 ? q[0] <= k + 1e-9 : q[0] >= k - 1e-9);
  const out = [];
  for (let i = 0; i < p.length; i++) {
    const a = p[i], b = p[(i + 1) % p.length];
    const ia = inside(a), ib = inside(b);
    if (ia) out.push(a);
    if (ia !== ib) { const t = (k - a[0]) / (b[0] - a[0]); out.push([k, a[1] + (b[1] - a[1]) * t]); }
  }
  return out;
}

Viewer coordinates to a story's plan

64-bit

The matrix from the viewer's Y-up meters to a story's plan millimeters.

M = (story world)⁻¹ × S × C⁻¹

Two general 4 × 4 inverses and two products; the axis swap and the factor 1000 are exact.

  • model-plan.js
  • lines 24 to 28
  • BimCraft 0.8.1
/** viewer (Y up, m) → model mm (Z up, file axes) → storey plan (mm). */
export function viewerToPlan(C, storeyWorldMm) {
  const S = [1000, 0, 0, 0, 0, 0, 1000, 0, 0, -1000, 0, 0, 0, 0, 0, 1];
  return mul4(inv4(storeyWorldMm), mul4(S, inv4(C)));
}
model-planview.js BimCraft 0.8.1 64-bit 1 calculation Reviewed

Tests None found by the review

Pointer position to plan millimeters

64-bit

The plan point under the pointer, which the drawing tools then snap and use.

x = (px − w ÷ 2) ÷ scale + cx; y = (h ÷ 2 − py) ÷ scale + cy

One 64-bit expression per axis from the pointer's position in CSS pixels, the view's scale and its center. The point's resolution is one pixel divided by the scale; zooming keeps the scale between 0.0005 and 20 pixels per millimeter.

  • model-planview.js
  • line 82
  • BimCraft 0.8.1
fromCanvas(px, py) { return [(px - this.w / 2) / this.scale + this.cx, (this.h2 / 2 - py) / this.scale + this.cy]; }

In detail

Method
Local coordinates along and across the wall; openings cut out with a one-line polygon clip.
Number types
64-bit numbers in millimeters.
Tolerances
Outline corners within 0.5 mm of the face line belong to the face. Stops rounded to 0.01 mm; chain pieces of 0.5 mm or less are not drawn. Clip side test ±1e-9 mm. Walls under 1 mm long and curved walls get no dimension.
Checked by
tools/bimcraft-test/model-test.mjs calls wallDims and wallPieces in Node.

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