Resources Numerical accuracy Calculation policy

Wall layers drawn in plans and sections

Works out the outline of each wall layer where a plan or a section cuts a wall, and the wall edges an elevation shows, as the linework drawn on screen and on sheets.

How the precision is kept

Layer boundaries are offsets from the wall's center line computed from the layer thicknesses in 64-bit millimeters, so a straight wall's layers are drawn at their exact offsets. Only curved walls are approximated, by chords that stay within 0.002 mm of the arc. Layers are drawn only when their thicknesses add up to the wall's thickness within 0.005 mm.

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.

wall-plan-graphics.js BimCraft 0.8.1 64-bit 6 calculations Reviewed

Tests None found by the review

Polygon clipped at a station or an offset

64-bit

Cuts a wall outline at an opening's edge or at a layer boundary.

t = (at − a) ÷ (b − a); new point has the clip coordinate exactly

The new point's clipped coordinate is set to the clip value itself and only the other one is interpolated, so layer boundaries stay at their exact offsets. A point within 1e-9 of the clip line counts as inside.

  • wall-plan-graphics.js
  • lines 15 to 24
  • BimCraft 0.8.1
function clip(poly, axis, at, side) {
  const out = [];
  for (let i = 0; i < poly.length; i++) {
    const a = poly[i], b = poly[(i + 1) % poly.length];
    const ia = side * (a[axis] - at) >= -1e-9, ib = side * (b[axis] - at) >= -1e-9;
    if (ia) out.push(a);
    if (ia !== ib) { const t = (at - a[axis]) / (b[axis] - a[axis]); out.push(a.map((v, j) => j === axis ? at : v + (b[j] - v) * t)); }
  }
  return out;
}

Layer bands across a wall in plan

64-bit

The band of each wall layer across the wall, from the exterior face inward.

off₀ = ±t ÷ 2; offᵢ₊₁ = offᵢ ∓ tᵢ

Each boundary is the previous one minus the layer's thickness, a running difference from the exterior face. A zero-thickness layer is drawn as a membrane line at its offset.

  • wall-plan-graphics.js
  • lines 101 to 112
  • BimCraft 0.8.1
const sign = wall.extSide === 'right' ? -1 : 1;
let off = sign * wall.t / 2;
const boundaries = new Map();
for (const [index, layer] of layers.entries()) {
  const end = off - sign * layer.t, material = byMaterial.get(layer.materialId);
  const polys = layer.t === 0 ? [] : pieces.map(p => clip(clip(p, 1, Math.min(off, end), 1), 1, Math.max(off, end), -1)).filter(p => p.length >= 3).map(p => mappedPath(p, map, true));
  const fill = layer.kind === 'cavity' ? 'none' : material.fill;
  const hatch = detail === 'fine' && ['diagonal', 'crosshatch'].includes(fill) ? polys.flatMap(p => hatchSegments(p, [], fill === 'crosshatch' ? 'cross' : 'diagonal', (scale || 50) / 75)) : [];
  row.layers.push({ index, materialId: layer.materialId, kind: layer.kind, thickness: layer.t, from: off, to: end, polys, fill, color: material?.color, fillColor: material?.fillColor, hatch });
  if (index > 0 && layer.t > 0 && !boundaries.has(off)) boundaries.set(off, { offset: off });
  if (layer.t === 0) boundaries.set(off, { offset: off, membrane: true, color: material.fillColor });
  off = end;

Layer thicknesses against the wall thickness

64-bit

Checks that a wall's layers add up to its thickness before any layer is drawn.

|Σ tᵢ − t| ≤ 0.005 mm

The thicknesses are added in order and compared with an absolute tolerance of 0.005 mm. A wall that fails is drawn as its outline only, with a warning.

  • wall-plan-graphics.js
  • line 87
  • BimCraft 0.8.1
if (!materials || !layers?.length || layers.some(l => !Number.isFinite(l.t) || l.t < 0 || (l.kind !== 'cavity' && !byMaterial.has(l.materialId))) || Math.abs(layers.reduce((s, l) => s + l.t, 0) - wall.t) > 0.005) {

Curved wall outlines as chords

64-bit

Maps a path from the wall's frame back to the plan, adding points along a curved wall so the chords stay close to the arc.

step = 2 acos(max(−1, 1 − 0.002 ÷ radius)); n = ⌈|Δstation| ÷ R ÷ step⌉

The chord's distance from the arc is at most 0.002 mm at the outer of the two offsets. The arccosine's argument is limited to −1. Straight walls are mapped point by point with no added points.

  • wall-plan-graphics.js
  • lines 61 to 74
  • BimCraft 0.8.1
function mappedPath(points, map, closed = false) {
  if (!map.geom.arc) return points.map(map.toW);
  const { R } = map.geom.arc, out = [];
  for (let i = 0; i < points.length; i++) {
    const a = points[i], b = points[(i + 1) % points.length];
    out.push(map.toW(a));
    if (!closed && i === points.length - 1) break;
    const radius = R + Math.max(Math.abs(a[1]), Math.abs(b[1]));
    const step = 2 * Math.acos(Math.max(-1, 1 - PLAN_CHORD_ERROR / radius));
    const n = Math.max(1, Math.ceil(Math.abs(b[0] - a[0]) / R / step));
    for (let j = 1; j < n; j++) out.push(map.toW(a.map((v, k) => v + (b[k] - v) * j / n)));
  }
  return out;
}

Boundary lines between layers

64-bit

Finds the stretches of a layer boundary that lie inside the wall's outline.

x = x_a + (x_b − x_a) × (offset − y_a) ÷ (y_b − y_a)

Crossings are sorted and each pair is kept when its middle is inside the outline or within 1e-7 mm of its edge. Stretches shorter than 1e-7 mm are dropped.

  • wall-plan-graphics.js
  • lines 26 to 43
  • BimCraft 0.8.1
// Includes a zero-width membrane on either outer face, where an even/odd
// interior-only segment clip would otherwise drop the upper boundary.
function boundarySegments(poly, offset) {
  const xs = [];
  for (let i = 0; i < poly.length; i++) {
    const a = poly[i], b = poly[(i + 1) % poly.length];
    if (Math.abs(a[1] - offset) < 1e-7) xs.push(a[0]);
    if ((a[1] < offset && b[1] > offset) || (a[1] > offset && b[1] < offset)) xs.push(a[0] + (b[0] - a[0]) * (offset - a[1]) / (b[1] - a[1]));
  }
  xs.sort((a, b) => a - b);
  const out = [];
  for (let i = 0; i < xs.length - 1; i++) {
    if (xs[i + 1] - xs[i] < 1e-7) continue;
    const x = (xs[i] + xs[i + 1]) / 2;
    if (inPoly(x, offset, poly) || poly.some((a, j) => segDist(x, offset, a, poly[(j + 1) % poly.length]) < 1e-7)) out.push([[xs[i], offset], [xs[i + 1], offset]]);
  }
  return out;
}

Outline of joined wall pieces

64-bit

Finds the outer edges of wall pieces that touch, so shared edges inside a joint are not drawn.

t = cross(c − a, v) ÷ cross(d, v); parallel when |cross(d, v)| < 1e-12 × |d| × |v|

The parallel test is scaled by both edge lengths. Each edge is cut at every crossing and a piece is kept when a point 0.001 mm (or a quarter of the piece) to one side is inside the pieces and the same point on the other side is not. End points keep their 64-bit coordinates; duplicates within 1e-7 mm are dropped.

  • wall-plan-graphics.js
  • lines 158 to 189
  • BimCraft 0.8.1
// Drawing union boundaries without node-grid quantization. The room loop walker's
// rounded station/grid keys can split a T junction across neighbouring bins.
// Open segments do not need that topology; endpoints retain binary64 coordinates.
function unionEdges(polys) {
  const edges = polys.flatMap(p => p.map((a, i) => [a, p[(i + 1) % p.length]]));
  const cross = (a, b) => a[0] * b[1] - a[1] * b[0], sub = (a, b) => [a[0] - b[0], a[1] - b[1]];
  const inside = p => polys.some(poly => inPoly(...p, poly)), out = [];
  for (const [a, b] of edges) {
    const d = sub(b, a), L = Math.hypot(...d); if (L < 1e-9) continue;
    const cuts = [0, 1];
    for (const [c, e] of edges) {
      const v = sub(e, c), ca = sub(c, a), den = cross(d, v);
      if (Math.abs(den) < 1e-12 * L * Math.hypot(...v)) {
        if (Math.abs(cross(ca, d)) > 1e-7 * L) continue;
        for (const q of [c, e]) { const t = ((q[0] - a[0]) * d[0] + (q[1] - a[1]) * d[1]) / (L * L); if (t > 0 && t < 1) cuts.push(t); }
      } else {
        const t = cross(ca, v) / den, u = cross(ca, d) / den;
        if (t > 0 && t < 1 && u >= -1e-9 && u <= 1 + 1e-9) cuts.push(t);
      }
    }
    cuts.sort((a, b) => a - b);
    for (let i = 0; i < cuts.length - 1; i++) {
      const t0 = cuts[i], t1 = cuts[i + 1]; if ((t1 - t0) * L < 1e-7) continue;
      const mid = a.map((v, j) => v + d[j] * (t0 + t1) / 2), h = Math.min(.001, (t1 - t0) * L / 4), n = [-d[1] / L * h, d[0] / L * h];
      if (inside(mid.map((v, j) => v + n[j])) === inside(mid.map((v, j) => v - n[j]))) continue;
      const p = a.map((v, j) => v + d[j] * t0), q = a.map((v, j) => v + d[j] * t1);
      const near = (x, y) => Math.hypot(x[0] - y[0], x[1] - y[1]) < 1e-7;
      if (!out.some(([x, y]) => (near(p, x) && near(q, y)) || (near(p, y) && near(q, x)))) out.push([p, q]);
    }
  }
  return out;
}
wall-section-graphics.js BimCraft 0.8.1 64-bit 5 calculations Reviewed

Tests None found by the review

Building width on an elevation

64-bit

The overall width of the building's walls across a view direction, for the width note on an elevation.

x = p · right ± |n · right| × t ÷ 2 over every wall end

A wall's thickness is projected onto the view by the cosine between the wall's normal and the view's right, so walls at any angle are measured to their faces. Curved walls and walls on a tilted level give no width.

  • wall-section-graphics.js
  • lines 17 to 30
  • BimCraft 0.8.1
/** Envelope width for the existing overall-elevation note. A wall's thickness
 * projects along its normal, not along both view axes unconditionally.
 * Curved envelopes remain outside this analytic slice.
 */
export function straightElevationWidth(walls, dir) {
  if(!walls.length||walls.some(w=>w.bulge||w.frameUnsupported))return null;
  const right=[dir[1],-dir[0]],xs=[];
  for(const w of walls) {
    const L=Math.hypot(w.b[0]-w.a[0],w.b[1]-w.a[1]);if(L<1)return null;
    const n=[-(w.b[1]-w.a[1])/L,(w.b[0]-w.a[0])/L],half=Math.abs(dot(n,right))*w.t/2;
    for(const p of [w.a,w.b])xs.push(dot(p,right)-half,dot(p,right)+half);
  }
  return {x0:Math.min(...xs),x1:Math.max(...xs)};
}

Wall and opening edges for an elevation

64-bit

The edges of a straight wall's two faces, its ends and its openings, handed to the hidden-line view.

P(s, t, h) = a + u s + n t at height h

Edges are built from the wall's own start, direction, thickness, height and openings in 64-bit, not from the mesh's triangles. Which of them are visible is still decided by the depth buffer of the mesh.

  • wall-section-graphics.js
  • lines 43 to 71
  • BimCraft 0.8.1
/** Surface edge candidates for the existing hidden-line projector. Mesh face
 * triangulation can contain unmatched subdivision edges around holes. Authored
 * straight walls instead supply their actual envelope and opening boundaries;
 * the unchanged triangle depth buffer still decides which edges are visible.
 */
export function wallSurfaceEdges(wall, openings) {
  if(wall.bulge||wall.frameUnsupported||wall.junctionUnsupported||!finite([...wall.a,...wall.b,wall.t,wall.height,wall.z??0]))return null;
  const L=Math.hypot(wall.b[0]-wall.a[0],wall.b[1]-wall.a[1]);if(L<1)return null;
  const u=[(wall.b[0]-wall.a[0])/L,(wall.b[1]-wall.a[1])/L],n=[-u[1],u[0]],z=wall.z??wall.baseOffset??0,top=z+wall.height;
  const ops=openings.filter(o=>o.wall===wall.id);if(ops.some(o=>!finite([o.offset,o.width,o.sill,o.height])))return null;
  const edges=[],P=(s,t,h)=>[wall.a[0]+u[0]*s+n[0]*t,wall.a[1]+u[1]*s+n[1]*t,h];
  for(const t of [-wall.t/2,wall.t/2]) {
    for(const h of [z,top]) {
      let spans=[[0,L]];
      for(const o of ops)if(h>=o.sill&&h<=o.sill+o.height)spans=spans.flatMap(([a,b])=>[[a,Math.min(b,o.offset)],[Math.max(a,o.offset+o.width),b]]).filter(([a,b])=>b>a);
      for(const [a,b]of spans)edges.push([P(a,t,h),P(b,t,h)]);
    }
    for(const s of [0,L])edges.push([P(s,t,z),P(s,t,top)]);
    for(const o of ops) {
      const a=o.offset,b=o.offset+o.width,lo=Math.max(z,o.sill),hi=Math.min(top,o.sill+o.height);
      if(hi<=lo)continue;
      edges.push([P(a,t,lo),P(a,t,hi)],[P(b,t,lo),P(b,t,hi)]);
      if(lo>z)edges.push([P(a,t,lo),P(b,t,lo)]);
      if(hi<top)edges.push([P(a,t,hi),P(b,t,hi)]);
    }
  }
  for(const s of [0,L])for(const h of [z,top])edges.push([P(s,-wall.t/2,h),P(s,wall.t/2,h)]);
  return edges;
}

Walls of every level in one level's axes

64-bit

Moves every wall and opening into one level's plan and height, for a section through several levels.

T = inverse(level) × level_wall; a′ = T a; z = T₁₄ + base offset

One 4×4 product per wall. A wall is marked unsupported when the combined matrix tilts by more than 1e-8 in the sum of its off-level terms, rather than being flattened.

  • wall-section-graphics.js
  • lines 73 to 89
  • BimCraft 0.8.1
/** All authored walls/openings into one storey's axes, including translated and
 * rotated levels. A tilted storey is unsupported rather than flattened silently.
 */
export function sectionModel(w, storey, keyBase = 0) {
  const I = inv4(w.storeyWorld(storey)), originals = readWalls(w), openings = readOpenings(w, null, originals);
  const walls = originals.map(wall => {
    const T = mul4(I, w.storeyWorld(wall.storey));
    const P = p => [T[0]*p[0]+T[4]*p[1]+T[12], T[1]*p[0]+T[5]*p[1]+T[13]];
    return { ...wall, key: keyBase + wall.id, a:P(wall.a), b:P(wall.b),
      z:T[14] + wall.baseOffset, frameUnsupported: Math.abs(T[2])+Math.abs(T[6])+Math.abs(T[8])+Math.abs(T[9])+Math.abs(T[10]-1)>1e-8 };
  });
  const ops = openings.map(o => {
    const T = mul4(I, w.storeyWorld(o.storey));
    return {...o,sill:o.sill+T[14]};
  });
  return {walls,openings:ops};
}

Where a section cuts a wall

64-bit

The station along a wall at which the section plane cuts it, when the cut is square to the wall.

s = ((o − a) · dir) ÷ (u · dir)

Only cuts with |u · dir| within 1e-8 of 1 are taken, so the divisor is ±1 to that tolerance. A cut within 1e-7 mm of the wall's end is treated as an end cut and left to the mesh.

  • wall-section-graphics.js
  • lines 103 to 110
  • BimCraft 0.8.1
const u = [(wall.b[0]-wall.a[0])/L,(wall.b[1]-wall.a[1])/L], n = [-u[1],u[0]], den = dot(u,dir);
// No claim to exact layered oblique/curved cuts. Only warn when mesh cuts it.
if (wall.bulge || wall.frameUnsupported || !isolatedWall(wall,walls) || Math.abs(Math.abs(den)-1)>1e-8) {
  if(cuts.some(c=>c.key===key)) warn('unsupported-cut');
  continue;
}
const s = dot([cut.o[0]-wall.a[0],cut.o[1]-wall.a[1]],dir)/den;
if (s <= 1e-7 || s >= L-1e-7) {if(cuts.some(c=>c.key===key))warn('end-cut');continue;}

Layer bands in a section cut

64-bit

The rectangle of each wall layer in a section, between the heights the openings leave.

x = c + off × (n · right); offᵢ₊₁ = offᵢ ∓ tᵢ

The wall's center in the view is one dot product; each layer boundary is a running difference of thicknesses times the sign of the wall's normal in the view. Rectangles are cut to the view's box by minimum and maximum only.

  • wall-section-graphics.js
  • lines 121 to 140
  • BimCraft 0.8.1
const c = dot([wall.a[0]+u[0]*s,wall.a[1]+u[1]*s],right), lateral = dot(n,right), sign=wall.extSide==='right'?-1:1;
const cropRect = (a,b,lo,hi) => {
  const x0=Math.max(Math.min(a,b),box?.x0??-Infinity),x1=Math.min(Math.max(a,b),box?.x1??Infinity);
  const y0=Math.max(lo,box?.y0??-Infinity),y1=Math.min(hi,box?.y1??Infinity);
  return x1>x0&&y1>y0 ? rect(x0,x1,y0,y1) : null;
};
const loops=spans.map(([a,b])=>cropRect(c-wall.t/2,c+wall.t/2,a,b)).filter(Boolean);
const row={key,guid:wall.guid,cls:'IfcWall',loops,source:'authored-wall',detail,scale,layers:undefined,boundaries:[]};
if(!valid) warn('unavailable-layers');
if(valid && detail!=='coarse') {
  row.layers=[];let off=sign*wall.t/2;
  const boundaries=new Map();
  for(const [index,l] of layers.entries()) {
    const end=off-sign*l.t,mat=materialById.get(l.materialId),fill=l.kind==='cavity'?'none':mat.fill;
    const polys=l.t?spans.map(([a,b])=>cropRect(c+off*lateral,c+end*lateral,a,b)).filter(Boolean):[];
    const hatch=detail==='fine'&&['diagonal','crosshatch'].includes(fill)?polys.flatMap(p=>hatchSegments(p,[],fill==='crosshatch'?'cross':'diagonal',(scale||50)/75)):[];
    row.layers.push({index,materialId:l.materialId,kind:l.kind,core:!!l.core,thickness:l.t,from:c+off*lateral,to:c+end*lateral,polys,fill,color:mat?.color,fillColor:mat?.fillColor,hatch});
    if(index>0&&l.t>0&&!boundaries.has(off))boundaries.set(off,{offset:c+off*lateral});
    if(l.t===0)boundaries.set(off,{offset:c+off*lateral,membrane:true,color:mat.fillColor});
    off=end;

In detail

Method
The wall's joined outline is mapped into the wall's own frame (station along, offset across), clipped at openings and at each layer boundary, and mapped back. A section replaces the mesh's cut face with rectangles from the wall's data when the cut is square to a straight wall.
Number types
64-bit numbers.
Tolerances
Layer sum against wall thickness: 0.005 mm. Chord error on curved walls: 0.002 mm. Clip side test: −1e-9. Boundary points: 1e-7 mm. A section cut counts as square to a wall when |cos| differs from 1 by at most 1e-8, and must be more than 1e-7 mm from the wall's ends. A level counts as tilted when its matrix differs from level by more than 1e-8.
Limits
Layered section cuts are made only for straight walls that touch no other wall and are cut square; other walls keep the mesh's cut face and a warning.

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