Resources Numerical accuracy Calculation policy

Interior elevations and their dimensions

Frames each wall of a room as an interior elevation, writes its width, height and level dimensions, and turns placed items.

How the precision is kept

Plan and view coordinates are millimeters in 64-bit. A dimension's value is one subtraction of two extents, handed to the unit formatter. The only snapping to a round value is the turn of a dragged item, which is by design.

Calculations under this policy

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

comforts-views.js BimCraft 0.8.1 64-bit 5 calculations Reviewed

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

Middle of a room

64-bit

The point the interior elevation cuts through.

middle = (min + max) ÷ 2 on each axis

The middle of the outline's bounding box, one addition and one halving per axis in 64-bit.

  • comforts-views.js
  • lines 39 to 44
  • BimCraft 0.8.1
function middle(poly) {
  // the middle of the room's box (the cut runs through it): robust for L-shaped rooms too
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
  for (const p of poly) { x0 = Math.min(x0, p[0]); y0 = Math.min(y0, p[1]); x1 = Math.max(x1, p[0]); y1 = Math.max(y1, p[1]); }
  return [(x0 + x1) / 2, (y0 + y1) / 2];
}

Frame of an interior elevation

64-bit

The width and height of the wall shown, and how deep the view looks.

x₀, x₁ = min, max of p·right; depth from the room's middle to max(p·dir) + 50 mm

Dot products of the outline's corners with the view's unit axes, then minimum and maximum, in 64-bit millimeters. The default height 2438.4 mm is 8 ft exactly. The 400 and 350 mm added round the frame are drawing margin only.

  • comforts-views.js
  • lines 46 to 57
  • BimCraft 0.8.1
/** One wall of a room, seen from inside it. room = { poly (plan, the room's inside outline), name }. */
export function interiorView(elements, room, side, { height = 2438.4, past = 50, res = 1400 } = {}) {
  const dir = LOOK[side], right = [dir[1], -dir[0]];
  if (!dir || !room?.poly?.length) return { lines: [], cuts: [], bounds: null, n: 0 };
  const xs = room.poly.map((p) => dot(p, right)), ds = room.poly.map((p) => dot(p, dir));
  const o = middle(room.poly), d0 = dot(o, dir), d1 = Math.max(...ds) + past;
  const x0 = Math.min(...xs), x1 = Math.max(...xs);
  // wall to wall and floor to ceiling, with room for the cut walls, the floor and the ceiling round it
  const box = { x0: x0 - 400, x1: x1 + 400, y0: -350, y1: height + 350 };
  const v = projectView(elements, { dir, cut: { o }, far: d1 - d0, box, res });
  return { ...v, side, dir, d0, d1, frame: { x0, x1, y0: 0, y1: height }, ground: true, room: room.name || '' };
}

Dimension values on elevations and sections

64-bit

The widths, heights and level spacings written on a view.

width = x₁ − x₀; height = y₁ − y₀; level spacing = yᵢ₊₁ − yᵢ

Each dimension's text is one 64-bit subtraction handed to the unit formatter; with no formatter it is rounded to a whole millimeter. Dimensions under 1 mm are left out. The 1 mm and 400 mm tolerances decide what gets a tag, not a value.

  • comforts-views.js
  • lines 59 to 103
  • BimCraft 0.8.1
/** The notes a view carries (see the header). Never throws on what it can't place: it leaves that note out. */
export function viewNotes(view, ctx = {}) {
  const out = [], b = view?.bounds;
  if (!b) return out;
  const fmt = ctx.fmt || ((mm) => String(Math.round(mm)));
  const dir = ctx.dir || view.dir, right = [dir[1], -dir[0]];
  const X = (p) => dot(p, right);
  const base = ctx.base || 0;
  if (ctx.kind === 'interior') {
    const f = view.frame;
    if (f && f.x1 - f.x0 > 1) out.push({ t: 'dim', a: [f.x0, f.y0], b: [f.x1, f.y0], off: -8, text: fmt(f.x1 - f.x0) });
    if (f && f.y1 - f.y0 > 1) out.push({ t: 'dim', a: [f.x0, f.y0], b: [f.x0, f.y1], off: 8, text: fmt(f.y1 - f.y0) });
    const inRange = (p, past = 1) => { const d = dot(p, dir); return d >= (ctx.d0 ?? view.d0) - 1 && d <= (ctx.d1 ?? view.d1) + past; };
    for (const o of ctx.openings || []) {
      // a door or window in the wall it looks at (its centre line lies inside that wall: up to a wall's thickness past its face)
      if (!o.mark || !inRange(o.at, 400) || (o.face && dot(o.face, dir) > -0.5 && dot(o.face, dir) < 0.5)) continue;
      const x = X(o.at);
      if (x < f.x0 - 1 || x > f.x1 + 1) continue;
      out.push({ t: 'tag', x, y: (o.z0 + o.z1) / 2, s: o.mark, shape: o.kind === 'door' ? 'box' : 'hex' });
    }
    for (const it of ctx.items || []) {
      if (!it.tag || !inRange(it.at)) continue;
      const x = X(it.at);
      if (x < f.x0 - 1 || x > f.x1 + 1) continue;
      out.push({ t: 'tag', x, y: Math.max(150, (it.z || 0) + (it.h || 900) / 2), s: String(it.tag), shape: 'circle' });
    }
    return out;
  }
  // elevations and sections: the levels that fall in the view
  const lv = (ctx.levels || []).map((l) => ({ ...l, y: l.elevation - base })).filter((l) => l.y >= b.y0 - 1 && l.y <= b.y1 + 600).sort((p, q) => p.y - q.y);
  const xr = b.x1 + 600;
  for (const l of lv) out.push({ t: 'level', y: l.y, x0: b.x0 - 300, x1: xr, name: l.name, text: fmt(l.elevation) });
  for (let i = 0; i + 1 < lv.length; i++) if (lv[i + 1].y - lv[i].y > 1) out.push({ t: 'dim', a: [b.x0, lv[i].y], b: [b.x0, lv[i + 1].y], off: 12, text: fmt(lv[i + 1].y - lv[i].y) });
  if (ctx.kind === 'elevation') {
    // the building's width (its walls, not the ground round it), under the ground line
    const w = ctx.width || (view.content || b);
    if (w.x1 - w.x0 > 1) out.push({ t: 'dim', a: [w.x0, b.y0], b: [w.x1, b.y0], off: -10, text: fmt(w.x1 - w.x0) });
    for (const o of ctx.openings || []) {
      // tags only on the walls that face the viewer (an elevation shows those)
      if (!o.mark || !o.face || dot(o.face, dir) > -0.7) continue;
      out.push({ t: 'tag', x: X(o.at), y: (o.z0 + o.z1) / 2, s: o.mark, shape: o.kind === 'door' ? 'box' : 'hex' });
    }
  }
  return out;
}

Where a door or window is tagged

64-bit

The plan point, the height range and the outward side of each opening.

middle station = offset + width ÷ 2; top = sill + height

Plain 64-bit sums in millimeters. The point and the normal come from the wall's own geometry function, which is passed in.

  • comforts-views.js
  • lines 105 to 116
  • BimCraft 0.8.1
/** The plan facts viewNotes needs about doors and windows, from model-ops.js snapshotModel openings and their walls. */
export function openingFacts(openings, walls, geom) {
  const out = [];
  for (const o of openings || []) {
    const w = (walls || []).find((x) => x.id === o.wall);
    if (!w) continue;
    const g = geom(w), s = o.offset + o.width / 2, at = g.at(s, 0), n = g.normal(s);
    const ext = w.extSide === 'right' ? -1 : 1;   // the wall's outside: its left (+n) or its right
    out.push({ mark: o.mark, kind: o.kind, at, z0: o.sill, z1: o.sill + o.height, face: [n[0] * ext, n[1] * ext], storey: w.storey, wall: w.guid, base: w.baseOffset || 0 });
  }
  return out;
}

Views placed at scale on the sheet

64-bit

Takes view millimeters to paper millimeters at the sheet's scale.

k = 1 ÷ scale ratio; paper = origin + view × k

One scale for all four views, chosen by res-sheets.js scaleFor; view millimeters are multiplied by its reciprocal in 64-bit. The offsets only center each view in its quarter of the sheet.

  • comforts-views.js
  • lines 178 to 185
  • BimCraft 0.8.1
const sc = scaleFor(W, H, [cw - 40, ch - 30], units);
const k = 1 / sc.ratio;
const placed = [];
shown.forEach((v, i) => {
  const col = i % 2, row = Math.floor(i / 2);
  const x = m + col * cw, y = m + strip + (1 - row) * ch;   // first row on top
  const b = v.view.bounds, ox = x + 22 + (cw - 40 - (b.x1 - b.x0) * k) / 2 - b.x0 * k, oy = y + 16 + (ch - 30 - (b.y1 - b.y0) * k) / 2 - b.y0 * k;
  const P = (q) => [ox + q[0] * k, oy + q[1] * k];
comforts-ui.js BimCraft 0.8.1 64-bit 2 calculations Reviewed

Tests None found by the review

Where a drag puts an item

64-bit

The position and turn saved when a placed item is dragged or turned by its grip.

turn = round(atan2(dy, dx) × 180 ÷ π ÷ step) × step, step 15° or 1° with Shift

The turn saved for a dragged item is a multiple of 15°, or of 1° with Shift, by design. A moved item takes the snapped plan point as the snap gives it.

  • comforts-ui.js
  • lines 68 to 78
  • BimCraft 0.8.1
/** Where a drag puts the item: { x, y, rot }. */
function dragTo(d, p, e) {
  const it = d.item;
  if (d.kind === 'rot') {
    const a = Math.atan2(p[1] - it.y, p[0] - it.x) / D2R, step = e?.shiftKey ? 1 : 15;
    return { x: it.x, y: it.y, rot: Math.round(a / step) * step };
  }
  const want = [it.x + p[0] - d.from[0], it.y + p[1] - d.from[1]];
  const s = A.snapFrom(want, e, [it.x, it.y]);
  return { x: s.p[0], y: s.p[1], rot: it.rot, snap: s };
}

Room height on the interior elevations

64-bit

The floor-to-floor height the elevations are framed to, and the height of each tagged item.

height = next level − this level, else the level's own height

The height is the difference of two level elevations in millimeters. An item's height is its stored Height times the file's unit in millimeters, or 900 mm when that is missing or zero.

  • comforts-ui.js
  • lines 143 to 146
  • BimCraft 0.8.1
const height = levels[i + 1] ? levels[i + 1].elevation - lv.elevation : lv.height;
const w = writer();
const facts = openingFacts(data.openings.filter((o) => o.storey === lv.id), data.walls, wallGeom);
const pl = w.placements(), its = placedItems(w, lv.id).map((p) => { const r = pl.find((x) => x.id === p.id); return { tag: p.itemNumber, at: [p.x, p.y], z: p.z, h: Number(r?.equipment?.Height || 0) * w.unitMm || 900 }; });

In detail

Method
A room's outline is projected on the view's right and depth axes; the frame is the minimum and maximum. Dimensions are differences of frame extents or of level elevations.
Number types
64-bit numbers.
Tolerances
Dimensions under 1 mm are left out. An opening is tagged when it lies within 1 mm of the frame's width and up to 400 mm past the wall face; an elevation tags openings whose wall faces the viewer by a dot product under −0.7.
Limits
The view cuts through the middle of the room's bounding box. A dragged item's turn is snapped to 15°, or 1° with Shift.

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