Turns the drawn beams, columns and braces into the nodes, members, sections, supports and loads the frame solver works on.
How the precision is kept
64-bit numbers throughout, but the model is put together with fixed millimeter tolerances: points within 1 mm become one node, and a point within 2 mm of a member's line splits that member. Section properties are computed from the profile outline with each fillet drawn in 16 steps. The solver itself is the shared frame library and is recorded separately.
Calculations under this policy
8 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.
One analysis node for every group of points that lie within 1 mm of each other.
same node when |p − n| ≤ 1 mm
The distance is Math.hypot of the three differences against an absolute 1 mm. The node keeps the coordinates of the first point that created it; later points within 1 mm take that position, so the result depends on the order the points arrive in.
analysis.js
lines 79 to 84
DetailCraft 0.8.1
const nodeAt = (p) => {
for (const n of nodes) if (dist(n.p, p) <= MERGE_TOL) return n;
const n = { id: 'n' + (nodes.length + 1), p: [...p] };
nodes.push(n);
return n;
};
Splitting a member at the points on it
64-bit
The stations along a drawn member where another member's end or a node load lies on it.
s = (q − start)·u; foot = start + u·s; cut when |foot − q| ≤ 2 mm and 2 mm < s < L − 2 mm
The unit direction is each difference divided by L from Math.hypot. Both tolerances are absolute. Stations closer than 1 mm to the one before are dropped, and the last station is set to L itself.
analysis.js
lines 121 to 131
DetailCraft 0.8.1
const d = sub(m.end, m.start), L = Math.hypot(...d), ux = d.map((v) => v / L);
const cuts = [0, L];
for (const q of points) {
const s = dot(sub(q, m.start), ux);
if (s <= SPLIT_TOL || s >= L - SPLIT_TOL) continue;
const foot = [m.start[0] + ux[0] * s, m.start[1] + ux[1] * s, m.start[2] + ux[2] * s];
if (dist(foot, q) <= SPLIT_TOL) cuts.push(s);
}
cuts.sort((a, b) => a - b);
const at = cuts.filter((s, i) => i === 0 || s - cuts[i - 1] > MERGE_TOL);
if (L - at[at.length - 1] > MERGE_TOL) at.push(L); else at[at.length - 1] = L;
End points of each piece
64-bit
The two points of every piece a split member becomes.
p = start + u·s
The first and last pieces use the stored start and end points unchanged; only inner points are computed from the station. Each point then goes through the 1 mm node merge.
analysis.js
lines 139 to 141
DetailCraft 0.8.1
const s0 = at[k], s1 = at[k + 1];
const pa = k === 0 ? m.start : m.start.map((v, i) => v + ux[i] * s0), pb = k + 2 === at.length ? m.end : m.start.map((v, i) => v + ux[i] * s1);
const ni = nodeAt(pa), nj = nodeAt(pb);
Section properties per profile
64-bit
Area, second moments of area and torsion constant of each profile in use, with the reduced bending stiffness of cracked concrete.
Iy′ = f·Iy, Iz′ = f·Iz
The properties come from the frame library's sectionOf on the outline with fillets in 16 steps, so curved corners are polygons. The cracked-concrete factor multiplies the two bending values only; area and torsion are left as computed.
Decides which column feet are held: those with a base plate, or with no member going lower at that node.
support when lowest z at the node ≥ node z − 1 mm
Heights are compared with an absolute 1 mm. The foot's node is found by the same 1 mm distance test as node merging.
analysis.js
lines 157 to 172
DetailCraft 0.8.1
const lowest = new Map(); // node id → the lowest z of any member end meeting there
for (const mb of members) {
const a = byId.get(mb.i).p[2], b = byId.get(mb.j).p[2];
lowest.set(mb.i, Math.min(lowest.get(mb.i) ?? Infinity, b)); lowest.set(mb.j, Math.min(lowest.get(mb.j) ?? Infinity, a));
}
const basePlated = new Set((state.connections || []).filter((c) => c.type === 'baseplate').map((c) => c.column));
const supports = new Map();
const baseFix = A.bases === 'pinned' ? [true, true, true, false, false, true] : [true, true, true, true, true, true];
let nBases = 0;
for (const { m } of drawn) {
if (m.type !== 'column') continue;
const foot = m.start[2] <= m.end[2] ? m.start : m.end;
const n = nodes.find((x) => dist(x.p, foot) <= MERGE_TOL);
if (!n || supports.has(n.id)) continue;
if (basePlated.has(m.id) || lowest.get(n.id) >= n.p[2] - MERGE_TOL) { supports.set(n.id, baseFix); nBases++; }
}
Point load on a split member
64-bit
Moves a point load from its distance along the drawn member to the piece it falls on.
a′ = max(0, a − s0)
The piece is the first whose range contains the station within 1e-9 mm; a station past the end goes on the last piece. The new distance is one subtraction, clamped at zero.
Cuts a uniform or linearly varying load into the part that falls on each piece.
w(s) = w + (w₂ − w)·(s − a) ÷ (b − a)
The intensity at each cut is one linear interpolation; b > a is tested before the division. Overlaps of 1e-6 mm or less are dropped.
analysis.js
lines 194 to 201
DetailCraft 0.8.1
const L = list[list.length - 1].s1;
const a = l.a ?? 0, b = l.b ?? L, w2 = l.w2 ?? l.w;
const wAt = (s) => (b > a ? l.w + (w2 - l.w) * (s - a) / (b - a) : l.w);
for (const q of list) {
const s0 = Math.max(a, q.s0), s1 = Math.min(b, q.s1);
if (s1 - s0 <= 1e-6) continue;
loads.push({ type: 'udl', member: members[q.k].id, dir: l.dir, w: wAt(s0), w2: wAt(s1), a: s0 - q.s0, b: s1 - q.s0 });
}
Sections the drawn members use
64-bit
The area and second moments of area listed for each profile in the model.
The same sectionOf call on the 16-step outline as the analysis uses; the values are passed on without rounding.
analysis.js
lines 227 to 240
DetailCraft 0.8.1
/** The sections the drawn members use, with their properties (the Concrete block sets up a rectangle from A and Iy). */
export function sectionsOf(state) {
const seen = new Map();
for (const m of state.members) {
if (!MEMBER_KINDS.includes(m.type) || m.panel) continue;
const key = String(m.profile).toUpperCase();
if (seen.has(key)) continue;
const p = getProfile(m.profile, state.profiles || []);
if (!p || !p.loops) continue;
const s = sectionOf({ ...p, loops: outline(p, 16) });
seen.set(key, { name: p.name, A: s.A, Iy: s.Iy, Iz: s.Iz });
}
return [...seen.values()];
}
In detail
Method
Members on their work lines, split where other member ends or node loads lie on them; one node per merged point; loads re-expressed on the split pieces.
Number types
64-bit numbers in plain arrays and objects.
Tolerances
Node merge 1 mm; split 2 mm from the member's line and from its ends; members shorter than 1 mm left out; vertical test |Δz| > 0.999999 × L; point-load station 1e-9 mm; load pieces shorter than 1e-6 mm dropped. All absolute, in millimeters.
Limits
A node sits at the first point that claimed it, so a member end up to 1 mm away is analyzed at that node's position. The comment in the code gives the effect of the 16-step fillets on the second moment of area as 0.01 %; this review did not check that figure.
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.