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