Resources Numerical accuracy Calculation policy

Site outline, building placement and levels

Stores the site boundary with its area and perimeter, places the building on the site, adds or splits levels, and turns a site-study mass into walls and floors.

How the precision is kept

Outline points are moved between a level's plan and the site's axes with 4×4 matrix products in 64-bit millimeters. Area and perimeter are computed from those points and stored through the writer's 6-decimal rounding. The outline is also kept as text rounded to 3 decimals of the file unit, and that text is what the app reads the outline back from.

Open item. Keep the site outline text at full precision so the outline read back matches the points the area and perimeter were computed from.

Calculations under this policy

11 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-site.js BimCraft 0.8.1 64-bit 8 calculations Reviewed

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

Outline area

64-bit

The signed area of the site outline.

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

The shoelace sum over absolute site coordinates in point order, with no compensation. For an outline far from the site origin the products are large and nearly cancel.

  • model-site.js
  • line 39
  • BimCraft 0.8.1
const area = (p) => { let s = 0; for (let i = 0; i < p.length; i++) { const a = p[i], b = p[(i + 1) % p.length]; s += a[0] * b[1] - a[1] * b[0]; } return s / 2; };

Site area and perimeter

64-bit

The GrossArea and GrossPerimeter stored for the site.

A = |shoelace|; P = Σ √(Δx² + Δy²)

Both are computed from the unrounded outline in site millimeters; the perimeter adds Math.hypot edge lengths in order. They are stored as a length rounded to 6 decimals of the file unit and an area in m² rounded to 6 decimals.

  • model-site.js
  • lines 120 to 121
  • BimCraft 0.8.1
const A = Math.abs(area(pts)), P = pts.reduce((sum, a, i) => { const b = pts[(i + 1) % pts.length]; return sum + Math.hypot(b[0] - a[0], b[1] - a[1]); }, 0);
w.upsertQto(sid, 'Qto_SiteBaseQuantities', [['GrossPerimeter', 'length', P], ['GrossArea', 'area', A]]);

Outline saved as text

64-bit

The site outline written to the CadCraft_Site Points property.

x,y;x,y;… each rounded to 3 decimals of the file unit

Each coordinate is rounded to 3 decimals of the file's length unit before it is written. readSite parses this text back, so the outline the app shows and edits later is the rounded one: 0.001 mm in a millimeter file, 1 mm in a meter file.

  • model-site.js
  • line 37
  • BimCraft 0.8.1
const ptsText = (p) => p.map((q) => round(q[0], 3) + ',' + round(q[1], 3)).join(';');

Building position and angle on the site

64-bit

Where the building sits on the site and how far it is turned.

M = site⁻¹ × building; at = (M₁₂, M₁₃); angle = atan2(M₁, M₀) × 180 ÷ π

One inverse and one product of 4×4 world matrices in millimeters. The angle uses Math.atan2 of the first column, so it does not depend on the column being exactly unit length.

  • model-site.js
  • lines 56 to 70
  • BimCraft 0.8.1
export function readSite(w) {
  const sid = siteOf(w);
  if (sid === null || sid === undefined) return null;
  const s = w.api.GetLine(w.mid, sid, false);
  const ps = w.readPset(sid, SITE_PSET);
  const points = ptsOf(ps?.Points).map((q) => [q[0] * w.unitMm, q[1] * w.unitMm]);
  const bid = building(w);
  let bInfo = null;
  if (bid !== undefined) {
    const bl = w.api.GetLine(w.mid, bid, false);
    const M = mul4(inv4(worldMm(w, s.ObjectPlacement?.value)), worldMm(w, bl.ObjectPlacement?.value));
    bInfo = { id: bid, at: [M[12], M[13]], angle: Math.atan2(M[1], M[0]) * 180 / Math.PI };
  }
  return { id: sid, guid: val(s.GlobalId), points, building: bInfo };
}

Plan points to site axes

64-bit

Moves points drawn on a level's plan into the site's own axes.

p′ = site⁻¹ × level × p

The two matrices are multiplied once and applied to each point with z = 0. siteToPlan is the same with the matrices exchanged.

  • model-site.js
  • lines 76 to 80
  • BimCraft 0.8.1
export function planToSite(w, storey, pts) {
  const sid = siteOf(w);
  const M = mul4(inv4(worldMm(w, sid ? w.api.GetLine(w.mid, sid, false).ObjectPlacement?.value : null)), w.storeyWorld(storey));
  return pts.map((q) => { const r = apply4(M, q[0], q[1], 0); return [r[0], r[1]]; });
}

Building placed on the site

64-bit

Writes the building's new position and turn relative to the site.

The building's height on the site is read from the current relative matrix and kept. The new x, y and angle are stored through the placement writer: coordinates to 6 decimals of the file unit, direction cosines to 12 decimals.

  • model-site.js
  • lines 138 to 139
  • BimCraft 0.8.1
const rel = mul4(inv4(worldMm(w, sl.ObjectPlacement?.value)), worldMm(w, lp));
w.movePlacement(lp, p[0], p[1], rel[14], angle);

Levels at given elevations

64-bit

Adds levels at heights above the building's base.

z = base + e; height = next e − e, at least 1000 mm

Each requested elevation is rounded to 0.001 mm, duplicates are removed and the list is sorted. A level is skipped when an existing one is within 1 mm of it. The last level takes the given height, else the height of the model's top level, else 3000 mm.

  • model-site.js
  • lines 152 to 173
  • BimCraft 0.8.1
export function addLevels({ elevations, height }) {
  const want = [...new Set(elevations.map((e) => Math.round(e * 1000) / 1000))].sort((a, b) => a - b);
  const guids = [];
  return op((w) => {
    if (!want.length) refuse('model.err.lv.none');
    let levels = readLevels(w);
    const base = levels.length ? levels[0].elevation : 0;
    const made = new Map(), out = [];
    want.forEach((e, i) => {
      const z = base + e;
      if (readLevels(w).some((l) => Math.abs(l.elevation - z) < 1)) return;
      levels = readLevels(w);
      const next = want[i + 1] !== undefined ? want[i + 1] - e : (height > 0 ? height : (levels[levels.length - 1]?.height || 3000));
      if (!guids[i]) guids[i] = val(w.guid());
      const r = addLevel(w, { name: levelName(levels, levels.length + 1), elevation: z, height: Math.max(1000, next), guid: guids[i] });
      made.set(r.guid, r.id); out.push(r.id);
    });
    if (!out.length) refuse('model.err.lv.none');
    regenAll(w);
    return { id: out[0], ids: out, made };
  });
}

Height split into equal levels

64-bit

Divides a building height into a number of equal levels.

h = T ÷ n; zᵢ = base + i × h

Each elevation is base + i × h, one multiplication and one addition, so rounding does not build up from level to level. The count is truncated with Math.floor and must be 1 to 200 with at least 1000 mm per level.

  • model-site.js
  • lines 175 to 193
  • BimCraft 0.8.1
export function splitLevels({ total, count }) {
  const T = Number(total), n = Math.floor(Number(count));
  const guids = [];
  return op((w) => {
    if (!(T >= 1000 * n) || !(n >= 1) || n > 200) refuse('model.err.lv.split');
    const levels = readLevels(w), order = levels.map((l) => l.guid);   // in their order now: moving one never reorders the rest
    const base = levels.length ? levels[0].elevation : 0, h = T / n;
    const made = new Map(), out = [];
    for (let i = 0; i < n; i++) {
      const z = base + i * h;
      if (order[i]) { setLevel(w, order[i], { elevation: z, height: h, shiftAbove: false }); out.push(w.byGuid(order[i])); continue; }
      if (!guids[i]) guids[i] = val(w.guid());
      const r = addLevel(w, { name: levelName(readLevels(w), i + 1), elevation: z, height: h, guid: guids[i] });
      made.set(r.guid, r.id); out.push(r.id);
    }
    regenAll(w);
    return { id: out[0], ids: out, made };
  });
}
model-site-mass.js BimCraft 0.8.1 64-bit 3 calculations Reviewed

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

Footprint of a site-study mass

64-bit

The outline of a mass in world millimeters, read from its extruded profile.

p = world matrix × (profile point × unit)

Profile coordinates are multiplied by the file unit and transformed by the placement's world matrix with z = 0. A last point within 1e-6 mm of the first is dropped as the closing point.

  • model-site-mass.js
  • lines 29 to 44
  • BimCraft 0.8.1
/** The footprint of a proxy: its extruded profile's outline, world mm. */
function footprintOf(w, id) {
  const line = w.api.GetLine(w.mid, id, true);
  const reps = line.Representation?.Representations || [];
  for (const r of reps) for (const it of r.Items || []) {
    const curve = it.SweptArea?.OuterCurve;
    if (!curve?.Points) continue;
    const local = curve.Points.map((p) => (p.Coordinates || []).map((c) => Number(val(c)) * w.unitMm)).filter((q) => q.length >= 2);
    const lp = line.ObjectPlacement?.value ?? line.ObjectPlacement?.expressID;
    const M = lp ? scaleT(w.api.GetWorldTransformMatrix(w.mid, lp), w.unitMm) : [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    let pts = local.map((q) => apply4(M, q[0], q[1], 0)).map((q) => [q[0], q[1]]);
    if (pts.length > 1 && Math.hypot(pts[0][0] - pts[pts.length - 1][0], pts[0][1] - pts[pts.length - 1][1]) < 1e-6) pts = pts.slice(0, -1);   // a closed polyline repeats its first point
    return pts;
  }
  return null;
}

A point inside the footprint

64-bit

Finds a point inside a possibly concave outline, used as the seed for the floor.

x = aₓ + (y − a_y) ÷ (b_y − a_y) × (bₓ − aₓ); seed = middle of the widest span

Scan lines are tried at seven fixed fractions of the outline's height and the widest span between crossings wins. The division by b_y − a_y is safe because an edge is used only when its ends lie on opposite sides of the scan line.

  • model-site-mass.js
  • lines 71 to 82
  • BimCraft 0.8.1
/** A point inside a (possibly concave) polygon: the middle of the widest stretch of the scan line through its centre. */
export function insidePoint(P) {
  const ys = P.map((p) => p[1]), y0 = Math.min(...ys), y1 = Math.max(...ys);
  let best = null;
  for (const f of [0.5, 0.4, 0.6, 0.3, 0.7, 0.2, 0.8]) {
    const y = y0 + (y1 - y0) * f, xs = [];
    for (let i = 0; i < P.length; i++) { const a = P[i], b = P[(i + 1) % P.length]; if ((a[1] > y) !== (b[1] > y)) xs.push(a[0] + ((y - a[1]) / (b[1] - a[1])) * (b[0] - a[0])); }
    xs.sort((p, q) => p - q);
    for (let k = 0; k + 1 < xs.length; k += 2) if (!best || xs[k + 1] - xs[k] > best.w) best = { w: xs[k + 1] - xs[k], p: [(xs[k] + xs[k + 1]) / 2, y] };
  }
  return best ? best.p : [P[0][0], P[0][1]];
}

Walls round the footprint

64-bit

Makes a wall along every edge of the mass footprint on each of its levels.

The footprint is moved into each level's plan with the inverse of the level's world matrix. Edges shorter than 1 mm get no wall. The top level's walls take the mass's floor height, the others follow the level above.

  • model-site-mass.js
  • lines 93 to 103
  • BimCraft 0.8.1
levels.forEach((l, k) => {
  const inv = inv4(w.storeyWorld(l.id));
  const plan = m.pts.map((p) => { const q = apply4(inv, p[0], p[1], 0); return [q[0], q[1]]; });
  const above = levels[k + 1];
  for (let i = 0; i < plan.length; i++) {
    const a = plan[i], b = plan[(i + 1) % plan.length];
    if (Math.hypot(b[0] - a[0], b[1] - a[1]) < 1) continue;
    const r = addWall(w, { storey: l.id, a, b, typeKey, topLevel: above ? above.guid : '', height: above ? 0 : (m.floorHeight || l.height), extSide: 'right', justification: 'exterior' });
    walls.push(r.id);
  }
});

In detail

Method
Shoelace area, summed edge lengths, rigid transforms by matrix product and inverse, equal division of a height into levels.
Number types
64-bit numbers; the saved outline text holds decimal numbers with 3 decimals of the file's length unit.
Tolerances
A level within 1 mm of an existing elevation is not added. Typed elevations are rounded to 0.001 mm. A mass edge shorter than 1 mm makes no wall. A mass footprint under 1 mm² is refused. A closing point within 1e-6 mm of the first is dropped. A site outline under 10000 mm² is refused (polyProblem in model-floors.js).
Limits
In a file whose unit is the meter the outline read back from the Points text is on a 1 mm grid, while the stored area and perimeter were computed from the unrounded points.

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.

Every policy that takes this approach