Reads a drawn wall's ends, length, thickness and height from the file and writes its shape, side areas, footprint and volumes.
How the precision is kept
A wall is stored as a start point, a direction and a Length; its end point is rebuilt from those on every read. Start and Length are written rounded to 6 decimals of the file unit and the direction to 12 decimals. Quantities are computed in 64-bit millimeters from the joined outline and stored through the same rounding. A curved wall's footprint and volumes come from its outline flattened to chords.
Open item. Compute a curved wall's footprint area and volumes from the arc outline rather than from its chords.
Calculations under this policy
12 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.
The layer thicknesses of a starter wall type made thicker or thinner.
thickest layer = wanted thickness − Σ other layers
The other layers keep their thickness and the thickest takes the difference. The wanted thickness must exceed the other layers by more than 1 mm and be at most 2000 mm.
model-walls.js
lines 63 to 81
BimCraft 0.8.1
/**
* A wall type by key: a starter, or a starter at another thickness — key "<starter>@<mm>" (Properties › Thickness): the
* thickest layer (the studs, the block) takes up the difference, the others keep theirs.
*/
export function wallType(key, source = null) {
const custom = (Array.isArray(source) ? source : readAssemblies(source)).find(x => x.key === key);
if (custom) return custom;
const k = String(key || '');
const base = WALL_TYPES.find((x) => x.key === k.split('@')[0]) || null;
if (!base) return null;
const coreIndex = base.layers.findIndex(l => ['studs', 'block', 'cmu', 'concrete'].includes(l.mat));
const recipe = { ...base, layers: base.layers.map((l, i) => normalizeLayer({ ...l, core: i === (coreIndex < 0 ? 0 : coreIndex), function: i === coreIndex ? 'structure' : l.mat === 'osb' ? 'substrate' : l.mat === 'insulation' ? 'insulation' : 'finish' })) };
if (!k.includes('@')) return recipe;
const want = Number(k.split('@')[1]);
const rest = base.layers.reduce((s, l) => s + l.t, 0) - Math.max(...base.layers.map((l) => l.t));
if (!(want > rest + 1 && want <= 2000)) return null;
const core = base.layers.indexOf(base.layers.reduce((a, b) => (b.t > a.t ? b : a)));
return { ...recipe, key: k, base: base.key, layers: recipe.layers.map((l, i) => (i === core ? { ...l, t: want - rest } : l)) };
}
Type key for a thickness
64-bit
Names the wall type that has a given total thickness.
key = base@(Math.round(mm × 10) ÷ 10)
A thickness within 0.05 mm of the starter type returns the starter type. Any other thickness is rounded to 0.1 mm when it goes into the type key, and the wall's stored thickness is then that rounded value.
model-walls.js
lines 82 to 89
BimCraft 0.8.1
/** The key of `type` made `mm` thick (null when its other layers alone are thicker than that). */
export function typeAt(typeKey, mm) {
const base = wallType(String(typeKey).split('@')[0]);
if (!base) return null;
if (Math.abs(typeThickness(base) - mm) < 0.05) return base.key;
const k = base.key + '@' + (Math.round(mm * 10) / 10);
return wallType(k) ? k : null;
}
Total thickness of a type
64-bit
The sum of a wall type's layer thicknesses.
t = Σ layer thickness
Layers are added in order from the exterior. Inch sizes such as 0.75 × 25.4 are 64-bit products of an exact factor.
The start point, end point, length and thickness of a wall as read back from the file.
a = (M₁₂, M₁₃); b = a + (M₀, M₁) ÷ |(M₀, M₁)| × chord; chord = chordFor(Length, Bulge)
The end point is not stored. It is rebuilt from the placement's origin and X direction relative to the level and the Length property, which were written rounded to 6 and 12 decimals. The direction is normalized again with Math.hypot before use.
model-walls.js
lines 328 to 334
BimCraft 0.8.1
const M = w.relMatrix(stLine.ObjectPlacement.value, lp);
const len = Number(ps.Length) * w.unitMm, th = Number(ps.Thickness) * w.unitMm;
// 0.6 round 2: a curved wall's Length is along its arc; its end is the chord away (Bulge = tan(θ / 4), + anticlockwise)
const bulge = Number(ps.Bulge ?? 0), chord = chordFor(len, bulge);
const a = [M[12], M[13]], dir = [M[0], M[1]];
const dl = Math.hypot(dir[0], dir[1]) || 1;
const b = [a[0] + dir[0] / dl * chord, a[1] + dir[1] / dl * chord];
Wall height
64-bit
A wall's height from the level its top follows, or its own fixed height.
h = (top elevation + top offset) − (base elevation + base offset)
The level-based height is used only when it is above 1 mm. Otherwise the fixed height is used, and when that is not positive the constant 2438.4 mm (96 × 25.4).
model-walls.js
lines 351 to 356
BimCraft 0.8.1
/** A wall's height: to the level its top follows (+ offset) when that level is there, else its own height. */
export function wallHeight(wall, levels, base = levels.find((l) => l.id === wall.storey)) {
const top = wall.topLevel ? levels.find((l) => l.guid === wall.topLevel) : null;
if (top && base) { const h = top.elevation + wall.topOffset - (base.elevation + wall.baseOffset); if (h > 1) return h; }
return wall.fixedHeight > 0 ? wall.fixedHeight : 2438.4;
}
Walls cut by another level's plan
64-bit
Finds walls of other levels that pass through a level's plan cut height.
base + 1 < level elevation + cut < top − 1 (mm)
Compares absolute heights with a fixed 1 mm margin at both ends of the wall.
A wall shorter than 1 mm is refused. The angle is passed in degrees and converted back to radians when the placement is written. Length, Thickness, Height and the offsets are rounded to 6 decimals of the file unit and Bulge to 9 decimals. A wall given no height gets 2438.4 mm.
model-walls.js
lines 420 to 438
BimCraft 0.8.1
const L = arcLength(a, b, bulge); // 0.6 round 2: along the arc for a curved wall
const st = w.api.GetLine(w.mid, storey, false);
if (!st || !/BuildingStorey/i.test(st.constructor?.name || '')) throw new Error(tr('place.err.pick_storey'));
const T = wallTypeLine(w, typeKey);
const th = typeThickness(T.type);
const ang = Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
const lp = w.placeAt(st.ObjectPlacement.value, a[0], a[1], baseOffset, ang);
const g = w.gid(guid);
const name = typeName(T.type);
const id = w.make('IFCWALL', g, w.ownerHistory(), w.L(name), null, w.L(name), w.H(lp), null, null, ...(w.v4 ? ['STANDARD'] : []));
w.noteGuid(g, id);
w.contain(id, storey);
w.relateType(id, T.id);
w.associateMaterial(id, usage(w, T.set, th, extSide));
w.upsertPset(id, WALL_PSET, [['TypeKey', 'IFCIDENTIFIER', typeKey], ['Length', 'IFCPOSITIVELENGTHMEASURE', round(w.u(L))], ['Thickness', 'IFCPOSITIVELENGTHMEASURE', round(w.u(th))],
['Height', 'IFCPOSITIVELENGTHMEASURE', round(w.u(height > 0 ? height : 2438.4))], ['BaseOffset', 'IFCLENGTHMEASURE', round(w.u(baseOffset))], ['TopLevel', 'IFCIDENTIFIER', topLevel || ''],
['TopOffset', 'IFCLENGTHMEASURE', round(w.u(topOffset))], ['ExteriorSide', 'IFCLABEL', extSide],
// 0.6 round 2: written only when they say something (a straight wall on its centre line keeps the 0.5 set)
...(bulge ? [['Bulge', 'IFCREAL', round(bulge, 9)]] : []), ...(justification && justification !== 'center' ? [['Justification', 'IFCLABEL', JUSTIFICATIONS.includes(justification) ? justification : 'center']] : [])]);
Moved wall: placement and length
64-bit
Rewrites a wall's start point, angle and Length after its ends move.
The angle is Math.atan2 of the new ends in degrees; the wall keeps the height coordinate read from its current placement matrix. Length is the arc length rounded to 6 decimals of the file unit and Bulge is rounded to 9 decimals.
model-walls.js
lines 542 to 548
BimCraft 0.8.1
function setGeom(w, wall, a, b) {
const ang = Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
const M = w.relMatrix(w.api.GetLine(w.mid, wall.storey, false).ObjectPlacement.value, wall.lp);
w.movePlacement(wall.lp, a[0], a[1], M[14], ang);
const bg = wall.bulge || 0; // 0.6 round 2: a curved wall keeps its turn; its Length runs along the arc
w.upsertPset(wall.id, WALL_PSET, [['Length', 'IFCPOSITIVELENGTHMEASURE', round(w.u(arcLength(a, b, bg)))], ...(bg || w.readPset(wall.id, WALL_PSET)?.Bulge !== undefined ? [['Bulge', 'IFCREAL', round(bg, 9)]] : [])]);
}
Whether a wall's shape is out of date
64-bit
Compares the stored outline, depth and axis length with the ones just computed.
|stored − computed| < 0.001 mm for depth, length and every outline point
SHAPE_TOL_MM is 0.001 mm, absolute. A change smaller than that in every value leaves the stored shape and quantities as they were.
model-walls.js
lines 583 to 602
BimCraft 0.8.1
export function regenWalls(w, storey, force = null) {
const walls = readWalls(w, storey);
if (!walls.length) return { walls: 0 };
const outlines = wallOutlines(walls.map((x) => ({ key: x.guid, a: x.a, b: x.b, t: x.t, bulge: x.bulge })));
const forced = new Set(force || []);
const host = w.hostIndex();
let n = 0;
for (const wall of walls) {
const poly = outlines.get(wall.guid);
if (!poly) continue;
const local = profileKeyPoints(toLocal(wall, poly.bulged || poly), w.v4);
if (!forced.has(wall.guid)) {
const cur = storedShape(w, wall.id);
if (cur && Math.abs(cur.depth - wall.height) < SHAPE_TOL_MM && Math.abs(cur.len - wall.len) < SHAPE_TOL_MM && samePoly(cur.points, local)) continue;
}
writeWallShape(w, wall, poly, host);
n++;
}
return { walls: n };
}
Outline in the wall's own axes
64-bit
Turns the plan outline into coordinates along and across the wall.
The start point is subtracted first, so the rotation works on small local differences. θ is Math.atan2 of the wall's ends.
model-walls.js
lines 603 to 606
BimCraft 0.8.1
const toLocal = (wall, poly) => {
const ang = Math.atan2(wall.b[1] - wall.a[1], wall.b[0] - wall.a[0]), c = Math.cos(ang), s = Math.sin(ang);
return poly.map(([x, y, bg]) => (bg ? [(x - wall.a[0]) * c + (y - wall.a[1]) * s, -(x - wall.a[0]) * s + (y - wall.a[1]) * c, bg] : [(x - wall.a[0]) * c + (y - wall.a[1]) * s, -(x - wall.a[0]) * s + (y - wall.a[1]) * c]));
};
Stored shape read back
64-bit
The outline, extrusion depth and axis length a wall's shape holds now.
arc axis length = R × |Δangle|
Coordinates are multiplied by the file unit to millimeters. For a curved axis the length is the radius times the difference of two Math.atan2 angles of the trim points, unwrapped by adding or subtracting 2π according to the curve's sense.
model-walls.js
lines 612 to 646
BimCraft 0.8.1
/** What the wall's shape holds now: its body outline (wall axes, mm, closing point dropped), extrusion depth, axis length. */
export function storedShape(w, id) {
const g = (x) => (x ? w.api.GetLine(w.mid, x.value ?? x, false) : null);
const line = g(w.api.GetLine(w.mid, id, false)?.Representation);
if (!line) return null;
let points = null, depth = null, len = null;
for (const h of line.Representations || []) {
const r = g(h);
const ident = val(r?.RepresentationIdentifier);
const item = g((r?.Items || [])[0]);
if (ident === 'Body' && item?.SweptArea) {
depth = Number(val(item.Depth)) * w.unitMm;
const curve = g(g(item.SweptArea)?.OuterCurve);
// 0.6 round 2: a curved wall's profile is an IfcIndexedPolyCurve (its point list: corners and the arcs' middles)
if (Array.isArray(curve?.Points)) points = curve.Points.map((p) => g(p).Coordinates.map((v) => Number(val(v)) * w.unitMm));
else points = (g(curve?.Points)?.CoordList || []).map((q) => q.map((v) => Number(val(v)) * w.unitMm));
if (points.length > 1 && dist(points[0], points[points.length - 1]) < 1e-6) points.pop();
} else if (ident === 'Axis' && item?.Points) {
const pts = item.Points.map((p) => g(p).Coordinates.map((v) => Number(val(v)) * w.unitMm));
len = pts.length > 1 ? dist(pts[0], pts[pts.length - 1]) : 0;
} else if (ident === 'Axis' && item?.BasisCurve) {
// an IfcTrimmedCurve of an IfcCircle: the arc between its two trimming points, the way it runs
const circ = g(item.BasisCurve), R = Number(val(circ?.Radius)) * w.unitMm, cpt = g(g(circ?.Position)?.Location)?.Coordinates?.map((v) => Number(val(v)) * w.unitMm);
const tp = (tr) => { const h = (tr || []).find((x) => x && typeof x === 'object' && x.type === 5 && g(x)?.Coordinates); return h ? g(h).Coordinates.map((v) => Number(val(v)) * w.unitMm) : null; };
const p1 = tp(item.Trim1), p2 = tp(item.Trim2);
if (R > 0 && cpt && p1 && p2) {
const sense = item.SenseAgreement === true || /^(\.?T\.?|true)$/i.test(String(val(item.SenseAgreement)));
let d = Math.atan2(p2[1] - cpt[1], p2[0] - cpt[0]) - Math.atan2(p1[1] - cpt[1], p1[0] - cpt[0]);
if (sense) { while (d <= 0) d += 2 * Math.PI; } else { while (d >= 0) d -= 2 * Math.PI; }
len = R * Math.abs(d);
}
}
}
return points ? { points, depth, len } : null;
}
Wall quantities
64-bit
Footprint area, side areas and volumes of a wall, with its openings taken off the net values.
footprint = |shoelace(outline)|; gross side = length × height; net side = max(0, gross side − Σ w·h); gross volume = footprint × height; net volume = max(0, gross volume − Σ w·h·t)
The footprint is the shoelace area of the joined outline in the wall's own axes; for a curved wall that outline is the chord version, not the arcs. Opening sizes are read from their stored properties, and each opening removes width × height × the full wall thickness. Net values are clamped at 0. All values then pass through the 6-decimal rounding of upsertQto.
End point = start + unit direction × chord; height = top level elevation + top offset − (base level elevation + base offset); footprint by the shoelace formula; volumes = footprint × height; net values subtract each opening's width × height (× wall thickness).
Number types
64-bit numbers. Layer thicknesses in inches are multiplied by 25.4, which is exact.
Tolerances
Shape rewrite threshold 0.001 mm on depth, length and each outline point. A wall must be at least 1 mm long. A height from levels is used when it is above 1 mm. Thickness at a custom size is rounded to 0.1 mm; a custom size within 0.05 mm of the starter type uses the starter type.
Limits
A curved wall's GrossFootprintArea, GrossVolume and NetVolume use the chord outline (flatPoly in model-arcs.js, whose arc step constant is 4°), not the true arcs. The end point of a wall is only as exact as the stored Length (6 decimals of the file unit).
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.