Works out a family's solids, plan symbol and connectors from its parameters and formulas, and writes them to the IFC file.
How the precision is kept
Parameters and expressions are evaluated in 64-bit millimeters and are not rounded while the family is evaluated. Writing rounds lengths to 6 decimals of the file's unit, directions to 12 decimals and angles to 9. Arcs in a profile are exact arcs in IFC4 files (start, middle and end point); in IFC2X3 files, in footprints and in plan symbols they are chords of 15° or 7.5°, and there the chord count sets how closely the arc is followed.
Calculations under this policy
21 calculations in 3 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.
Lengths, angles and numbers are kept as given when finite and no larger than 1e7 in size; otherwise 0. Integers are rounded with Math.round, pens are rounded and held between 1 and 16, and Yes / No becomes 1 or 0.
family-model.js
lines 99 to 110
BimCraft 0.8.1
/** A parameter's value as its kind keeps it (untrusted): numbers finite and bounded, integers whole, pens 1–16, words cleaned. */
export function cleanValue(kind, value) {
if (kind === 'text') return cleanText(value, 120);
if (kind === 'material') { const s = String(value ?? '').trim(); return MATERIAL_RE.test(s) ? s : ''; }
if (kind === 'fill') return FILL_KEYS.includes(value) ? value : 'none';
const v = Number(value);
if (!Number.isFinite(v) || Math.abs(v) > LIMITS.value) return kind === 'pen' ? 1 : 0;
if (kind === 'yesno') return v ? 1 : 0;
if (kind === 'integer') return Math.round(v);
if (kind === 'pen') return Math.min(16, Math.max(1, Math.round(v)));
return v;
}
When two sizes are the same
64-bit
The key that tells one size of a family from another.
key = every input value rounded to 0.001
Values are rounded to 3 decimals only for the comparison. Two sizes whose inputs differ by less than that are one size, and the element takes the values of the size that was made first.
family-model.js
lines 416 to 420
BimCraft 0.8.1
/** What makes two sizes the same: their input values to 0.001. */
export function sizeKey(fam, values) {
const v = sizeValues(fam, values);
return Object.keys(v).sort().map((k) => k + '=' + (typeof v[k] === 'string' ? JSON.stringify(v[k]) : round(v[k], 3))).join(';');
}
Parameters driven by formulas
64-bit
The value of every parameter that has a formula.
Formulas are evaluated in passes: one is evaluated once every name it uses has a value, and a pass that settles nothing is reported as a cycle. Results are kept unrounded in 64-bit.
family-model.js
lines 437 to 449
BimCraft 0.8.1
let pending = fam.params.filter((p) => p.formula);
const names = [...fam.params.filter((p) => !isWordKind(p.kind)).map((p) => p.key), ...Object.keys(env)];
while (pending.length) {
const next = [];
for (const p of pending) {
const ps = parse(p.formula, names);
if (!ps.ok) return { ok: false, error: err('fam.err.expr', { where: p.key, why: ps.error }) };
if (ps.uses.some((u) => !(u in env))) { next.push(p); continue; }
env[p.key] = evaluate(ps.ast, env);
}
if (next.length === pending.length) return { ok: false, error: err('fam.err.cycle', { name: next[0].key }) };
pending = next;
}
A solid's numbers
64-bit
The evaluated box, profile, depth, axis, angle, path and radius of each solid, and the check that it is not flat.
box = (min, max) of each pair; a negative depth moves the start: at = at + depth, depth = −depth
Fields are evaluated in 64-bit and kept as they come. The flatness limits are absolute: 0.01 mm for a side, a depth, a radius or a path step, 1 mm² for a profile, and an angle must be above 0.01° and at most 360°.
family-model.js
lines 466 to 490
BimCraft 0.8.1
if (s.op === 'box') {
const v = ['x0', 'y0', 'z0', 'x1', 'y1', 'z1'].map((k) => num(s[k], where + ' ' + k));
e.box = [Math.min(v[0], v[3]), Math.min(v[1], v[4]), Math.min(v[2], v[5]), Math.max(v[0], v[3]), Math.max(v[1], v[4]), Math.max(v[2], v[5])];
if (visible && (e.box[3] - e.box[0] < 0.01 || e.box[4] - e.box[1] < 0.01 || e.box[5] - e.box[2] < 0.01)) return { ok: false, error: err('fam.err.flat', { name: where }) };
} else if (s.op === 'extrude' || s.op === 'revolve') {
e.plane = s.plane; e.at = num(s.at, where + ' at');
e.profile = s.profile.map((p, i) => [num(p[0], where + ' ' + (i + 1)), num(p[1], where + ' ' + (i + 1)), p[2] || 0]);
if (visible && Math.abs(polyArea(e.profile)) < 1) return { ok: false, error: err('fam.err.flat', { name: where }) };
if (s.op === 'extrude') {
e.depth = num(s.depth, where + ' depth');
if (e.depth < 0) { e.at += e.depth; e.depth = -e.depth; }
if (visible && e.depth < 0.01) return { ok: false, error: err('fam.err.flat', { name: where }) };
} else {
e.axis = s.axis.map((a, i) => num(a, where + ' axis ' + (i + 1)));
e.angle = num(s.angle, where + ' angle');
if (visible && !(e.angle > 0.01 && e.angle <= 360)) return { ok: false, error: err('fam.err.angle', { name: where }) };
if (visible && !oneSide(e.profile, e.axis)) return { ok: false, error: err('fam.err.axis', { name: where }) };
}
} else {
e.path = s.path.map((p, i) => p.map((x) => num(x, where + ' ' + (i + 1))));
e.radius = num(s.radius, where + ' radius');
if (visible && !(e.radius > 0.01)) return { ok: false, error: err('fam.err.flat', { name: where }) };
if (visible && e.path.some((p, i) => i && Math.hypot(p[0] - e.path[i - 1][0], p[1] - e.path[i - 1][1], p[2] - e.path[i - 1][2]) < 0.01)) return { ok: false, error: err('fam.err.flat', { name: where }) };
}
solids.set(s.id, e);
Copies in an array
64-bit
The solids of an array, each moved by a whole number of steps.
copy i is moved by (dx × i, dy × i, dz × i), count = round(count expression)
Each copy's offset is the step times its index, a single product, so the last copy does not carry rounding added up over the earlier ones. The count is rounded to a whole number and must be between 1 and 200.
family-model.js
lines 492 to 498
BimCraft 0.8.1
const n = Math.round(num(s.array.count, where + ' array-count'));
if (!(n >= 1 && n <= LIMITS.array)) return { ok: false, error: err('fam.err.arraycount', { name: where, max: LIMITS.array }) };
const d = ['dx', 'dy', 'dz'].map((k) => num(s.array[k], where + ' array ' + k));
if (solids.size + n - 1 > LIMITS.instances) return { ok: false, error: err('fam.err.toomany', { what: 'solids', n: LIMITS.instances }) };
const ids = [s.id];
for (let i = 1; i < n; i++) { const id = s.id + '~' + i; solids.set(id, shifted(e, id, d[0] * i, d[1] * i, d[2] * i)); ids.push(id); }
instances.set(s.id, ids);
A family placed inside another
64-bit
The position and quarter-turn of a nested family.
turns = rotation ÷ 90; accepted when round(turns × 10⁶) ÷ 10⁶ is a whole number; k = turns mod 4
The rotation must be a multiple of 90° to within 1e-6 of a quarter turn. The turn is then applied k times by rot90, which swaps and negates coordinates, so no sine or cosine is involved and the turned solid is exact.
family-model.js
lines 524 to 526
BimCraft 0.8.1
const at = p.at.map((x, i) => num(x, where + ' at ' + 'xyz'[i])), turns = p.rot !== undefined ? num(p.rot, where + ' rot') / 90 : 0;
if (!Number.isInteger(Math.round(turns * 1e6) / 1e6)) return { ok: false, error: err('fam.err.nestrot', { name: where }) };
const k = ((Math.round(turns) % 4) + 4) % 4;
Quarter turn of a solid
Exact
An evaluated solid turned 90° about the vertical axis.
(x, y) → (−y, x)
Only swaps and sign changes of 64-bit numbers, which are exact. A vertical-plane extrusion moves to the other vertical plane with its start at −(at + depth), one addition.
family-model.js
lines 593 to 607
BimCraft 0.8.1
/** An evaluated solid turned a quarter turn about z ((x, y) → (−y, x)); null for a revolve on a vertical plane (its direction would not carry over). */
function rot90(e) {
const c = { ...e };
if (e.op === 'box') { const b = e.box; c.box = [-b[4], b[0], b[2], -b[1], b[3], b[5]]; return c; }
if (e.op === 'sweep') { c.path = e.path.map((p) => [-p[1], p[0], p[2]]); return c; }
if (e.plane === 'xy') {
c.profile = e.profile.map((p) => [-p[1], p[0], p[2]]);
if (e.op === 'revolve') c.axis = [-e.axis[1], e.axis[0], -e.axis[3], e.axis[2]];
return c;
}
if (e.op === 'revolve') return null;
if (e.plane === 'xz') { c.plane = 'yz'; c.at = -(e.at + e.depth); return c; } // volume y ∈ [at, at + depth] becomes x ∈ [−(at + depth), −at]
c.plane = 'xz'; c.profile = e.profile.map((p) => [-p[0], p[1], -(p[2] || 0)]); // yz: x' = −y mirrors u (arcs turn the other way), y' = x
return c;
}
Move a solid
64-bit
An evaluated solid moved by an offset in the family's axes.
p → p + (dx, dy, dz), mapped to the profile plane's own axes
One addition per coordinate. A bulge is carried over unchanged.
family-model.js
lines 608 to 619
BimCraft 0.8.1
/** An evaluated solid moved by (dx, dy, dz) in the family's axes (a copy, under a new id). */
function shifted(e, id, dx, dy, dz) {
const c = { ...e, id };
if (e.op === 'box') { const b = e.box; c.box = [b[0] + dx, b[1] + dy, b[2] + dz, b[3] + dx, b[4] + dy, b[5] + dz]; return c; }
if (e.op === 'sweep') { c.path = e.path.map((p) => [p[0] + dx, p[1] + dy, p[2] + dz]); return c; }
// extrusions and revolves: the plane's own axes (xy: u = x, v = y; xz: u = x, v = z; yz: u = y, v = z) and what is normal to it
const [du, dv, dn] = e.plane === 'xy' ? [dx, dy, dz] : e.plane === 'xz' ? [dx, dz, dy] : [dy, dz, dx];
c.at = e.at + dn;
c.profile = e.profile.map((p) => [p[0] + du, p[1] + dv, p[2]]);
if (e.op === 'revolve') c.axis = [e.axis[0] + du, e.axis[1] + dv, e.axis[2] + du, e.axis[3] + dv];
return c;
}
Area of a profile
64-bit
The area of a profile with its arcs, used to refuse a flat outline.
area = ½ Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ) with arcs flattened to chords
Arcs are flattened to chords of at most 15° first, so the area is that of the chord polygon. The code's comment says it is enough to tell a real outline from a flat one: the result is only compared with 1 mm².
family-model.js
lines 621 to 628
BimCraft 0.8.1
/** A profile's area with its arcs (each arc flattened to chords; enough to tell a real outline from a flat one). */
function polyArea(pts) {
const flat = [];
pts.forEach((p, i) => { flat.push(p); if (p[2]) flat.push(...arcPoints(p, pts[(i + 1) % pts.length], p[2])); });
let a = 0;
for (let i = 0; i < flat.length; i++) { const p = flat[i], q = flat[(i + 1) % flat.length]; a += p[0] * q[1] - q[0] * p[1]; }
return a / 2;
}
Is the profile on one side of the axis
64-bit
Whether a revolve's profile stays on one side of its axis.
s = ((axis direction) × (p − axis start)) ÷ |axis|; sides counted beyond ±0.01 mm
The signed distance of each profile point from the axis line, divided by the axis length. An axis shorter than 1e-6 mm is refused, which guards the division. Points within 0.01 mm of the axis count as on it.
family-model.js
lines 629 to 636
BimCraft 0.8.1
/** Every profile point on one side of the revolve axis (or on it). */
function oneSide(pts, ax) {
const dx = ax[2] - ax[0], dy = ax[3] - ax[1], L = Math.hypot(dx, dy);
if (L < 1e-6) return false;
let pos = 0, negs = 0;
for (const p of pts) { const s = (dx * (p[1] - ax[1]) - dy * (p[0] - ax[0])) / L; if (s > 0.01) pos++; else if (s < -0.01) negs++; }
return !(pos && negs) && (pos || negs);
}
Middle point of a profile arc
64-bit
The point halfway along an arc given by its two ends and a bulge.
Closed form from the bulge, with no trigonometry. The division by the chord length is not guarded in this function: two points at the same place give 0 ÷ 0.
family-model.js
lines 672 to 676
BimCraft 0.8.1
/** The arc's middle point: the chord's middle pushed out by the sagitta (bulge = tan(θ / 4), + = counter-clockwise). */
export function arcMid(a, b, bulge) {
const dx = b[0] - a[0], dy = b[1] - a[1], c = Math.hypot(dx, dy), s = bulge * c / 2;
return [(a[0] + b[0]) / 2 + dy / c * s, (a[1] + b[1]) / 2 - dx / c * s];
}
Chords of a profile arc
64-bit
The points inside an arc, for outlines that cannot hold a true arc.
θ = 4·atan(bulge); n = max(2, ⌈|θ| ÷ 15°⌉); center = circle through start, middle and end
The center is found from the three points by the circumcircle formula, whose terms are squares of the coordinates as given; far from the family's origin they are large and nearly cancel. A divisor below 1e-9 in absolute value returns no points. The chords span at most 15°.
family-model.js
lines 677 to 691
BimCraft 0.8.1
/** Points strictly inside an arc from a to b (chords for a polyline). */
export function arcPoints(a, b, bulge) {
const th = 4 * Math.atan(bulge), n = Math.max(2, Math.ceil(Math.abs(th) / (Math.PI / 12)));
const m = arcMid(a, b, bulge);
// the circle through a, m, b
const ax = a[0], ay = a[1], bx = m[0], by = m[1], cx = b[0], cy = b[1];
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
if (Math.abs(d) < 1e-9) return [];
const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
const r = Math.hypot(ax - ux, ay - uy), a0 = Math.atan2(ay - uy, ax - ux);
const out = [];
for (let i = 1; i < n; i++) { const t = a0 + th * i / n; out.push([ux + r * Math.cos(t), uy + r * Math.sin(t)]); }
return out;
}
Profile written to the file
64-bit
A closed profile as an IFC curve: lines and true arcs in IFC4, a polyline in IFC2X3.
In IFC4 an arc is written as three points (start, middle, end), each rounded to 6 decimals of the file's unit, so the arc stays an arc. In IFC2X3 the arcs are flattened to chords of at most 15° and the solid is that of the chord outline.
family-model.js
lines 645 to 671
BimCraft 0.8.1
/** A closed profile (mm; bulges = arcs): IFC4 IfcIndexedPolyCurve with IfcArcIndex segments, else an IfcPolyline. */
function profileCurve(w, pts, flipV) {
const P = pts.map((p) => [p[0], flipV ? -p[1] : p[1], flipV ? -(p[2] || 0) : (p[2] || 0)]);
const arcs = P.some((p) => p[2]);
if (arcs && w.v4) {
const list = [], segs = [];
const add = (x, y) => { list.push([round(w.u(x)), round(w.u(y))]); return list.length; };
const first = add(P[0][0], P[0][1]);
for (let i = 0; i < P.length; i++) {
const a = P[i], b = P[(i + 1) % P.length], i0 = i === 0 ? first : list.length;
if (!a[2]) { const i1 = (i + 1) % P.length === 0 ? first : add(b[0], b[1]); segs.push(w.T('IFCLINEINDEX', [i0, i1])); continue; }
const m = arcMid(a, b, a[2]);
const im = add(m[0], m[1]), i1 = (i + 1) % P.length === 0 ? first : add(b[0], b[1]);
segs.push(w.T('IFCARCINDEX', [i0, im, i1]));
}
const pl = w.make('IFCCARTESIANPOINTLIST2D', list);
return w.make('IFCINDEXEDPOLYCURVE', w.H(pl), segs, false);
}
// straight pieces; arcs as chords (IFC2X3 has no arc segments in a profile's polyline), 24 to a full turn
const flat = [];
for (let i = 0; i < P.length; i++) {
const a = P[i], b = P[(i + 1) % P.length];
flat.push([a[0], a[1]]);
if (a[2]) for (const q of arcPoints(a, b, a[2])) flat.push(q);
}
return w.polyline2(flat, true);
}
A solid written to the file
64-bit
One box, extrusion, revolve or swept solid as its IFC item.
The revolve axis direction is rounded to 12 decimals, the angle to 9 decimals and lengths to 6 decimals of the file's unit. The division by the axis length is guarded earlier by the one-side check, which refuses an axis shorter than 1e-6 mm for a visible solid.
The outline other programs see in plan: the family's symbol, or each solid's outline from above.
A box and a horizontal extrusion give their true outline, with arcs as chords of at most 15°. Every other solid gives the rectangle of its plan bounds, which for a revolve or a sweep is a rectangle round it, not its outline.
family-model.js
lines 960 to 980
BimCraft 0.8.1
function footprint(w, ev, fam) {
if (fam?.symbol?.parts.length && ev.symbol?.length) { // the family's own symbol is what other programs see in plan too
const curves = symbolCurves(ev.symbol);
if (curves.length) return w.make('IFCGEOMETRICCURVESET', curves.map((c) => w.H(w.polyline2(c.pts, c.closed))));
}
const loops = [];
for (const s of ev.solids.values()) {
if (!s.visible) continue;
if (s.op === 'box') { const [x0, y0, , x1, y1] = s.box; loops.push([[x0, y0], [x1, y0], [x1, y1], [x0, y1]]); }
else if (s.op === 'extrude' && s.plane === 'xy') {
const flat = [];
s.profile.forEach((a, i) => { flat.push([a[0], a[1]]); if (a[2]) flat.push(...arcPoints(a, s.profile[(i + 1) % s.profile.length], a[2])); });
loops.push(flat);
} else {
const b = planBounds(s);
if (b) loops.push([[b[0], b[1]], [b[2], b[1]], [b[2], b[3]], [b[0], b[3]]]);
}
}
if (!loops.length) return null;
return w.make('IFCGEOMETRICCURVESET', loops.map((l) => w.H(w.polyline2(l, true))));
}
Plan bounds of a solid
64-bit
The smallest plan rectangle round a solid.
revolve: R = largest distance of a profile point from the axis; bounds = axis start ± R, joined with the profile
Minimum and maximum by exact comparison. An extrusion's bounds use its profile points only, so an arc that bulges outward lies outside them. A revolve's bounds are the axis start plus and minus the largest distance of a profile point from the axis, joined with the profile points; the code's comment calls this conservative.
family-model.js
lines 981 to 997
BimCraft 0.8.1
/** A solid's plan bounds [x0, y0, x1, y1] (conservative for revolves: the profile swept a whole turn). */
export function planBounds(s) {
if (s.op === 'box') return [s.box[0], s.box[1], s.box[3], s.box[4]];
if (s.op === 'sweep') { const xs = s.path.map((p) => p[0]), ys = s.path.map((p) => p[1]); return [Math.min(...xs) - s.radius, Math.min(...ys) - s.radius, Math.max(...xs) + s.radius, Math.max(...ys) + s.radius]; }
const P = s.profile.map((p) => planeTo3(s.plane, s.at, p[0], p[1]));
if (s.op === 'extrude') {
const d = s.plane === 'xy' ? [0, 0, s.depth] : s.plane === 'xz' ? [0, s.depth, 0] : [s.depth, 0, 0];
const all = [...P, ...P.map((p) => [p[0] + d[0], p[1] + d[1], p[2] + d[2]])];
return [Math.min(...all.map((p) => p[0])), Math.min(...all.map((p) => p[1])), Math.max(...all.map((p) => p[0])), Math.max(...all.map((p) => p[1]))];
}
const A = planeTo3(s.plane, s.at, s.axis[0], s.axis[1]), B = planeTo3(s.plane, s.at, s.axis[2], s.axis[3]);
const L = Math.hypot(B[0] - A[0], B[1] - A[1], B[2] - A[2]) || 1, d = [(B[0] - A[0]) / L, (B[1] - A[1]) / L, (B[2] - A[2]) / L];
let R = 0;
for (const p of P) { const v = [p[0] - A[0], p[1] - A[1], p[2] - A[2]], k = v[0] * d[0] + v[1] * d[1] + v[2] * d[2]; R = Math.max(R, Math.hypot(v[0] - k * d[0], v[1] - k * d[1], v[2] - k * d[2])); }
const xs = P.map((p) => p[0]), ys = P.map((p) => p[1]);
return [Math.min(A[0] - R, ...xs), Math.min(A[1] - R, ...ys), Math.max(A[0] + R, ...xs), Math.max(A[1] + R, ...ys)];
}
The evaluated lines, rectangles, circles, arcs, outlines and texts of a family's 2D symbol.
Each field is an expression evaluated in 64-bit and kept unrounded. A parameter printed inside a text is rounded to 3 decimals for the text only. Array copies are moved by the step times the index.
family-symbol.js
lines 102 to 134
BimCraft 0.8.1
/** The visible parts with their numbers worked out. `env` = the numbers, `texts` = the words (text, material and fill parameters). Throws ExprError with `where`. */
export function evalSymbol(sym, env, texts = {}, { rank = 2 } = {}) {
const out = [];
const num = (src, where) => {
const p = parse(src);
if (!p.ok) throw Object.assign(new ExprError(p.error), { where });
try { return evaluate(p.ast, env); } catch (e) { if (e instanceof ExprError) e.where = where; throw e; }
};
const fillOf = (f) => (f === undefined ? 'none' : FILL_KEYS.includes(f) ? f : (FILL_KEYS.includes(texts[f]) ? texts[f] : 'none'));
for (const p of sym?.parts || []) {
const where = 'symbol ' + p.id;
if (p.detail && detailRank(p.detail) > rank) continue; // finer than asked for
if (p.visible !== undefined && num(p.visible, where + ' visible') === 0) continue;
const e = { id: p.id, kind: p.kind, pen: p.pen !== undefined ? clampPen(num(p.pen, where + ' pen')) : 3, fill: fillOf(p.fill) };
if (p.zmin !== undefined) e.zmin = num(p.zmin, where + ' zmin');
if (p.zmax !== undefined) e.zmax = num(p.zmax, where + ' zmax');
for (const k of SYMBOL_FIELDS[p.kind] || []) e[k] = num(p[k], where + ' ' + k);
if (p.kind === 'poly') { e.pts = p.pts.map((pt, i) => [num(pt[0], where + ' point ' + (i + 1)), num(pt[1], where + ' point ' + (i + 1))]); e.closed = p.closed; }
if (p.kind === 'text') {
e.rot = p.rot !== undefined ? num(p.rot, where + ' rot') : 0;
e.text = p.text.replace(NAME_PLACEHOLDER, (m, name) => (name in texts ? texts[name] : name in env ? String(Math.round(env[name] * 1000) / 1000) : m));
}
if (p.array) {
const n = Math.round(num(p.array.count, where + ' array-count'));
if (!(n >= 1 && n <= SYMBOL_LIMITS.parts)) throw Object.assign(new ExprError('array-count'), { where: where + ' array-count' });
const dx = num(p.array.dx, where + ' array-dx'), dy = num(p.array.dy, where + ' array-dy');
for (let i = 0; i < n; i++) out.push(i === 0 ? e : moved(e, dx * i, dy * i));
continue;
}
out.push(e);
}
return out;
}
Chords of at most 7.5°. This is also what the file's FootPrint keeps for a family with a symbol, so other programs see the chords, not the arc. Angles are degrees × π ÷ 180 with Math.PI.
family-symbol.js
lines 143 to 149
BimCraft 0.8.1
const arcPts = (cx, cy, r, a0, a1, step = 7.5) => {
let sweep = a1 - a0;
while (sweep <= 0) sweep += 360;
const n = Math.max(2, Math.ceil(sweep / step)), pts = [];
for (let i = 0; i <= n; i++) { const a = (a0 + sweep * i / n) * Math.PI / 180; pts.push([cx + r * Math.cos(a), cy + r * Math.sin(a)]); }
return pts;
};
A symbol placed in the plan
64-bit
The symbol turned by the element's rotation and moved to its place.
(u, v) → (x + u·cos a − v·sin a, y + u·sin a + v·cos a), a = rotation × π ÷ 180
One rotation and one translation per point in 64-bit. A rotation of 90° gives a cosine of about 6e-17 instead of 0, so a point 1000 mm out is off by about 6e-14 mm. The pen's line weight is 0.5 + 0.2 × pen, rounded to 2 decimals.
family-symbol.js
lines 182 to 213
BimCraft 0.8.1
/**
* The plan's scene items for one placed element (model-planview.js scene.symbols): the symbol turned by `rot` degrees and put at (x, y), one
* item per pen so each draws at its own weight; cut parts as lines, below as thin, overhead as dashed; a solid fill as `solid`, other fills as
* hatch strokes (`hatch(ring, pattern)` → [x1, y1, x2, y2, …], from drafting.js); texts; the first item also carries `poly` (the symbol's
* extent) so a click on the symbol picks the element under `key`. → [] when nothing shows.
*/
export function symbolScene(parts, { x = 0, y = 0, rot = 0 } = {}, cutZ, key, hatch = () => []) {
const a = rot * Math.PI / 180, c = Math.cos(a), s = Math.sin(a);
const T = ([u, v]) => [x + u * c - v * s, y + u * s + v * c];
const byPen = new Map();
const slot = (pen) => { if (!byPen.has(pen)) byPen.set(pen, { key, lines: [], thin: [], dashed: [], solid: [], texts: [], lw: Math.round((0.5 + pen * 0.2) * 100) / 100 }); return byPen.get(pen); };
const all = [];
for (const it of symbolGeometry(parts, cutZ)) {
const S = slot(it.pen);
if (it.kind === 'text') { const p = T(it.at); S.texts.push({ p, h: it.h, text: it.text, rot: rot + it.rot }); all.push(p); continue; }
const pts = it.pts.map(T);
all.push(...pts);
const dest = it.rule === 'cut' ? S.lines : it.rule === 'below' ? S.thin : S.dashed;
const n = it.kind === 'ring' ? pts.length : pts.length - 1;
for (let i = 0; i < n; i++) { const p = pts[i], q = pts[(i + 1) % pts.length]; dest.push([p[0], p[1], q[0], q[1]]); }
if (it.kind === 'ring' && it.fill !== 'none' && it.rule !== 'above') {
if (it.fill === 'solid') S.solid.push(pts);
else { const h = hatch(pts, it.fill); for (let i = 0; i + 3 < h.length; i += 4) S.thin.push([h[i], h[i + 1], h[i + 2], h[i + 3]]); }
}
}
const out = [...byPen.values()];
if (out.length && all.length) {
const xs = all.map((p) => p[0]), ys = all.map((p) => p[1]), x0 = Math.min(...xs), x1 = Math.max(...xs), y0 = Math.min(...ys), y1 = Math.max(...ys);
out[0].poly = [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
}
return out;
}
A placed element's position and rotation in its level's plan.
x = M₁₂, y = M₁₃, z = M₁₄; rotation = atan2(M₁, M₀) × 180 ÷ π
Read from the element's placement relative to its level, as a 4 × 4 matrix. The rotation goes through atan2 and a conversion to degrees, so an element placed at 90° can read back as a value one rounding step away from 90.
family-ops.js
lines 127 to 145
BimCraft 0.8.1
/** Placed unhosted family elements, in their storey's plan (mm, degrees). */
export function readFamilyElements(w) {
const idx = familyIndex(w), out = [];
const so = w.storeyOf(), stl = new Map(); // one pass over the containment relations (0.7: a scan per element was quadratic: 2,000 elements took 1.6 s)
for (const [tid, s] of idx.sizes) {
const g = idx.groups.get(s.family);
if (!g?.fam || g.fam.host === 'wall') continue;
for (const id of idx.occ.get(tid) || []) {
const l = w.api.GetLine(w.mid, id, false);
if (!l?.ObjectPlacement) continue;
const storey = so.get(id) ?? null;
if (storey && !stl.has(storey)) stl.set(storey, w.api.GetLine(w.mid, storey, false));
const st = storey ? stl.get(storey) : null;
const M = st?.ObjectPlacement ? w.relMatrix(st.ObjectPlacement.value, l.ObjectPlacement.value) : null;
out.push({ id, guid: val(l.GlobalId), family: s.family, typeId: tid, size: s.name, values: s.values, instance: readInstance(w, id, g.fam), storey, x: M ? M[12] : 0, y: M ? M[13] : 0, z: M ? M[14] : 0, rot: M ? Math.atan2(M[1], M[0]) * 180 / Math.PI : 0 });
}
}
return out;
}
Connector written as a port
64-bit
A family's pipe or duct connector written on a placed element: its point, direction and nominal size.
The point and direction are the evaluated values; the direction is one of the six axis directions, so it is exact. The nominal size is rounded to 6 decimals of the file's length unit when written.
Box, extrusion, revolve and sweep, joined or cut by Booleans, written as IfcExtrudedAreaSolid, IfcRevolvedAreaSolid, IfcSweptDiskSolid and IfcBooleanResult. A profile arc is given by a bulge = tan(θ ÷ 4). Formulas are evaluated by expr.js in the order their inputs become known. Nested families are turned in quarter turns by swapping coordinates.
Number types
64-bit numbers in millimeters and degrees.
Tolerances
A solid thinner than 0.01 mm, a profile under 1 mm² and a revolve angle of 0.01° or less are refused. A revolve profile may cross its axis by no more than 0.01 mm. Two sizes are the same size when every input agrees to 0.001. A nested family's rotation must be a multiple of 90° to 1e-6 of a quarter turn. Values beyond ±1e7 are refused.
Limits
IFC2X3 profiles and footprints flatten arcs to chords of at most 15° (24 to a full turn); plan symbols use 7.5°. Plan bounds ignore the arcs of an extrusion's profile and take a revolve as a full turn.
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.