Resources Numerical accuracy Calculation policy

Curved walls

Works out the circle, length and points of a wall whose center line is an arc, where doors and windows sit in it, and the curves it is saved with.

How the precision is kept

An arc is kept as its two ends and a bulge (the tangent of a quarter of its angle); radius, center and length are closed-form 64-bit expressions. Where a polygon is needed the arc is cut into chords of at most 4°. The radius and the profile points written to the file are rounded to 6 decimals of the file's unit.

Calculations under this policy

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

model-arcs.js BimCraft 0.8.1 64-bit 11 calculations Reviewed

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

Circle of an arc

64-bit

The center, radius, angle and length of the arc between two points with a bulge.

θ = 4 atan(b); h = b c ÷ 2; R = ((c ÷ 2)² + h²) ÷ (2 |h|); length = R |θ|

Closed form in 64-bit. For a nearly straight arc the sagitta is tiny and the radius very large, and the center is then placed a radius away from the arc's middle, so points found later as center + R × cos subtract nearly equal large numbers. A bulge of 1e-9 or less is treated as straight.

  • model-arcs.js
  • lines 40 to 54
  • BimCraft 0.8.1
/** The circle of the arc from a to b with this bulge (null when it is straight or a and b are one point). */
export function arcOf(a, b, bulge) {
  const bg = Number(bulge) || 0;
  const dx = b[0] - a[0], dy = b[1] - a[1], c = Math.hypot(dx, dy);
  if (!(Math.abs(bg) > 1e-9) || c < 1e-9) return null;
  const theta = 4 * Math.atan(bg);
  const nl = [-dy / c, dx / c];
  const h = bg * c / 2;   // sagitta: a + bulge bows to the right of a → b (its centre is on the left)
  const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
  const mid = [m[0] - nl[0] * h, m[1] - nl[1] * h];
  const R = ((c / 2) ** 2 + h * h) / (2 * Math.abs(h));
  const sg = Math.sign(theta);
  const C = [mid[0] + nl[0] * R * sg, mid[1] + nl[1] * R * sg];
  return { C, R, theta, sg, a0: Math.atan2(a[1] - C[1], a[0] - C[0]), len: R * Math.abs(theta), chord: c, mid };
}

Chord of an arc of a given length

64-bit

The straight distance between the ends of an arc of a given length and bulge.

chord = len × sin(θ ÷ 2) ÷ (θ ÷ 2)

One sine and a division by the half angle; below 1e-9 rad the arc length itself is returned, which avoids 0 ÷ 0.

  • model-arcs.js
  • lines 56 to 60
  • BimCraft 0.8.1
/** The chord of an arc `len` long with this bulge (its included angle). */
export function chordFor(len, bulge) {
  const th = Math.abs(4 * Math.atan(Number(bulge) || 0));
  return th < 1e-9 ? len : len * Math.sin(th / 2) / (th / 2);
}

Arc height and bulge

64-bit

How far the arc's middle stands off its chord, and the bulge for a typed height.

height = −b × c ÷ 2; b = −2 h ÷ c

A product and a division by the chord length; a zero chord gives a bulge of zero.

  • model-arcs.js
  • lines 67 to 68
  • BimCraft 0.8.1
export const arcHeight = (a, b, bulge) => -(Number(bulge) || 0) * dist(a, b) / 2;
export const bulgeFromHeight = (a, b, h) => { const c = dist(a, b); return c > 0 ? -2 * h / c : 0; };

Arc through three points

64-bit

The bulge of the arc from a through p to b.

d = 2 (a_x (p_y − b_y) + p_x (b_y − a_y) + b_x (a_y − p_y)); bulge = tan(θ ÷ 4)

The circle's center comes from the determinant form with the points' absolute coordinates squared, so digits are lost as the points get far from the origin compared with their spacing. Three points count as in a line when the determinant is under 1e-9 × (|ab|² + 1), a test scaled to the chord.

  • model-arcs.js
  • lines 69 to 80
  • BimCraft 0.8.1
/** The bulge of the arc from a through p to b (0 when the three are in a line). */
export function bulgeThrough(a, p, b) {
  const d = 2 * (a[0] * (p[1] - b[1]) + p[0] * (b[1] - a[1]) + b[0] * (a[1] - p[1]));
  if (Math.abs(d) < 1e-9 * (dist(a, b) ** 2 + 1)) return 0;
  const A2 = a[0] ** 2 + a[1] ** 2, P2 = p[0] ** 2 + p[1] ** 2, B2 = b[0] ** 2 + b[1] ** 2;
  const C = [(A2 * (p[1] - b[1]) + P2 * (b[1] - a[1]) + B2 * (a[1] - p[1])) / d, (A2 * (b[0] - p[0]) + P2 * (a[0] - b[0]) + B2 * (p[0] - a[0])) / d];
  const ang = (q) => Math.atan2(q[1] - C[1], q[0] - C[0]);
  const norm = (x) => ((x % TAU) + TAU) % TAU;
  const ccw = norm(ang(b) - ang(a)), dp = norm(ang(p) - ang(a));
  const theta = dp < ccw ? ccw : -(TAU - ccw);
  return Math.tan(theta / 4);
}

Chords of an arc

64-bit

The points between an arc's ends when it is drawn or saved as straight pieces.

n = max(1, ⌈|θ| ÷ step⌉); point = C + R (cos f, sin f)

The angle is divided evenly into ⌈|θ| ÷ 4°⌉ pieces and each point is the center plus R at that angle, so the points lie on the arc and the chords stand inside it. The comment beside the 4° step says under 3 mm off a 5 m radius; R × (1 − cos 2°) gives 3.05 mm.

  • model-arcs.js
  • lines 81 to 89
  • BimCraft 0.8.1
/** The points strictly between p and q on the arc (none when straight), one per `step` of angle at most. */
export function arcPoints(p, q, bulge, step = ARC_STEP) {
  const k = arcOf(p, q, bulge);
  if (!k) return [];
  const n = Math.max(1, Math.ceil(Math.abs(k.theta) / step));
  const out = [];
  for (let i = 1; i < n; i++) { const f = k.a0 + k.theta * i / n; out.push([k.C[0] + k.R * Math.cos(f), k.C[1] + k.R * Math.sin(f)]); }
  return out;
}

Points along and off a wall

64-bit

A point at a distance along a wall and an offset to its left, and the reverse, for straight and curved walls.

curved: p = C + (R − off × sg) (cos φ, sin φ), φ = a₀ + sg × s ÷ R

Straight: unit vectors from the ends, with a zero length replaced by 1e-9. Curved: polar coordinates round the arc's center; the distance along is R times an angle wrapped into (−π, π] round the arc's middle, so a point just past either end stays continuous.

  • model-arcs.js
  • lines 101 to 125
  • BimCraft 0.8.1
/** One answer for straight and curved walls: length, points along it and off it, the tangent, where a point lies. */
export function wallGeom(wall) {
  const a = wall.a, b = wall.b, k = arcOf(a, b, wall.bulge);
  if (!k) {
    const L = dist(a, b) || 1e-9, u = [(b[0] - a[0]) / L, (b[1] - a[1]) / L], n = [-u[1], u[0]];
    return {
      arc: null, L, chord: L,
      at: (s, off = 0) => [a[0] + u[0] * s + n[0] * off, a[1] + u[1] * s + n[1] * off],
      tan: () => u, normal: () => n,
      param: (p) => ({ s: dot(sub(p, a), u), off: dot(sub(p, a), n) }),
    };
  }
  const { C, R, sg, a0, theta } = k, half = Math.abs(theta) / 2;
  const phi = (s) => a0 + sg * s / R;
  return {
    arc: k, L: k.len, chord: k.chord,
    at: (s, off = 0) => { const f = phi(s), r = R - off * sg; return [C[0] + r * Math.cos(f), C[1] + r * Math.sin(f)]; },
    tan: (s) => { const f = phi(s); return [-sg * Math.sin(f), sg * Math.cos(f)]; },
    normal: (s) => { const f = phi(s); return [-sg * Math.cos(f), -sg * Math.sin(f)]; },
    param: (p) => {
      const d = (Math.atan2(p[1] - C[1], p[0] - C[0]) - a0) * sg;
      return { s: R * (wrap(d - half) + half), off: (R - dist(p, C)) * sg };
    },
  };
}

Where an opening sits in a curved wall

64-bit

The position and angle of a door or window in the wall's own axes, square to the wall at the opening's middle.

O = at(off + w ÷ 2) − T × w ÷ 2; angle = atan2(T) − chord angle

The opening's origin is the arc point at its middle moved back half its width along the tangent, then turned into the wall's axes with one cos and sin. The angle is the tangent's direction minus the chord's, in degrees.

  • model-arcs.js
  • lines 140 to 150
  • BimCraft 0.8.1
/**
 * Where an opening `width` wide whose near edge is `off` along the wall sits in the wall's axes: { x, y, ang (degrees) }.
 * Straight: (off, 0, 0). Curved: square to the wall at the opening's middle, its X along the tangent there.
 */
export function hostFrame(wall, off, width) {
  if (!arcOf(wall.a, wall.b, wall.bulge)) return { x: off, y: 0, ang: 0 };
  const g = wallGeom(wall), sc = off + width / 2, M = g.at(sc, 0), T = g.tan(sc);
  const O = [M[0] - T[0] * width / 2, M[1] - T[1] * width / 2];
  const ax = wallAxes(wall), o = ax.toLocal(O);
  return { x: o[0], y: o[1], ang: (Math.atan2(T[1], T[0]) - ax.ang) * 180 / Math.PI };
}

Offset of an opening read back

64-bit

The distance along a curved wall of an opening, from the matrix that places it.

offset = s(middle) − w ÷ 2

The opening's middle is rebuilt from its matrix, taken to plan coordinates and projected on the arc; half the width is subtracted. The X axis of the matrix is divided by its length, with a zero length replaced by 1.

  • model-arcs.js
  • lines 151 to 157
  • BimCraft 0.8.1
/** The offset of an opening from the matrix that places it in its wall (mm, the wall's axes; model-write.js relMatrix). */
export function hostOffset(wall, M, width) {
  if (!arcOf(wall.a, wall.b, wall.bulge)) return M[12];
  const xl = Math.hypot(M[0], M[1]) || 1, X = [M[0] / xl, M[1] / xl];
  const ax = wallAxes(wall), c = ax.toWorld([M[12] + X[0] * width / 2, M[13] + X[1] * width / 2]);
  return wallGeom(wall).param(c).s - width / 2;
}

Opening body in a curved wall

64-bit

The four corners of the body that cuts an opening through a curved wall, with jambs along the radius.

half = w ÷ R ÷ 2; outer radius = r ÷ cos(half)

The corners on the convex face are pushed out by 1 ÷ cos(half the opening's angle) so the straight edge between them clears the curve. The division by that cosine is not guarded for an opening that spans 180° of the arc.

  • model-arcs.js
  • lines 158 to 175
  • BimCraft 0.8.1
/**
 * A curved wall's opening body as a profile in the opening's axes (hostFrame): its jambs along the radius at both ends of the
 * opening, through the wall `proud` past each face — the convex face's corners pushed out so the straight edge between them
 * still clears the curve. null for a straight wall (the opening is the usual box).
 */
export function openingCut(wall, op, proud = 10) {
  const k = arcOf(wall.a, wall.b, wall.bulge);
  if (!k) return null;
  const g = wallGeom(wall), t = wall.t || 0, s0 = op.offset, s1 = op.offset + op.width, sc = (s0 + s1) / 2;
  const half = op.width / k.R / 2, y = t / 2 + proud;
  const pt = (s, off) => {
    const r = k.R - off * k.sg, f = k.a0 + k.sg * s / k.R, rr = r > k.R ? r / Math.cos(half) : r;
    return [k.C[0] + rr * Math.cos(f), k.C[1] + rr * Math.sin(f)];
  };
  const M = g.at(sc, 0), T = g.tan(sc), N = [-T[1], T[0]], O = [M[0] - T[0] * op.width / 2, M[1] - T[1] * op.width / 2];
  const loc = (p) => [dot(sub(p, O), T), dot(sub(p, O), N)];
  return [loc(pt(s0, -y)), loc(pt(s1, -y)), loc(pt(s1, y)), loc(pt(s0, y))];
}

Axis curve saved for a curved wall

64-bit

The circle and its two trim points written as the wall's axis.

circle of radius R about the center in the wall's axes, trimmed from (0, 0) to (chord, 0)

The radius is rounded to 6 decimals of the file's unit before it is written; the center and trim points go through the writer's point routine.

  • model-arcs.js
  • lines 178 to 185
  • BimCraft 0.8.1
/** The Axis of a curved wall: an IfcTrimmedCurve of an IfcCircle in the wall's axes, from (0, 0) to (chord, 0). */
export function arcAxisCurve(w, wall) {
  const k = arcOf(wall.a, wall.b, wall.bulge);
  const c = wallAxes(wall).toLocal(k.C);
  const pos = w.make('IFCAXIS2PLACEMENT2D', w.H(w.point2mm(c[0], c[1])), null);
  const circle = w.make('IFCCIRCLE', w.H(pos), round(w.u(k.R)));
  return w.make('IFCTRIMMEDCURVE', w.H(circle), [w.H(w.point2mm(0, 0))], [w.H(w.point2mm(k.chord, 0))], k.sg > 0, 'CARTESIAN');
}

Profile with arc edges

64-bit

The closed outline of a curved wall written with true arcs (IFC4) or chords (IFC2X3).

arc edge = start, middle of the arc, end

Each corner and each arc's middle point is rounded to 6 decimals of the file's unit. In IFC2X3 files the arcs are written as 4° chords.

  • model-arcs.js
  • lines 186 to 201
  • BimCraft 0.8.1
/** A closed profile curve whose points may carry a bulge: IFC4 IfcIndexedPolyCurve (lines and IfcArcIndex arcs), IFC2X3 an
 *  IfcPolyline of chords. */
export function profileCurve(w, pts) {
  if (!pts.some((p) => p[2]) || !w.v4) return w.polyline2(flatPoly(pts), true);
  const list = [], segs = [];
  const add = (q) => { list.push([round(w.u(q[0])), round(w.u(q[1]))]); return list.length; };
  const first = add(pts[0]);
  for (let i = 0; i < pts.length; i++) {
    const p = pts[i], q = pts[(i + 1) % pts.length], i0 = i === 0 ? first : list.length;
    const last = i === pts.length - 1;
    if (!p[2]) { const i1 = last ? first : add(q); segs.push(w.T('IFCLINEINDEX', [i0, i1])); continue; }
    const im = add(arcMid(p, q, p[2])), i1 = last ? first : add(q);
    segs.push(w.T('IFCARCINDEX', [i0, im, i1]));
  }
  return w.make('IFCINDEXEDPOLYCURVE', w.H(w.make('IFCCARTESIANPOINTLIST2D', list)), segs, false);
}

In detail

Method
θ = 4 atan(bulge); sagitta h = bulge × chord ÷ 2; R = ((chord ÷ 2)² + h²) ÷ (2 |h|); length = R |θ|. A point along the wall is the center plus R at the angle a₀ ± s ÷ R.
Number types
64-bit numbers in plan millimeters.
Tolerances
A bulge of at most 1e-9 or a chord under 1e-9 mm is a straight wall (absolute). Three points are in a line when the determinant is under 1e-9 × (chord² + 1). Chords span at most 4°.
Checked by
tools/bimcraft-test/comforts-test.mjs calls arcOf, bulgeThrough, chordFor, flatPoly and wallGeom in Node.
Limits
A 4° chord stands R × (1 − cos 2°) off the arc, which is 3.05 mm at a 5 m radius. A wall turns at most three quarters of a circle.

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