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