A part's solid
64-bitBuilds the side faces and end caps of one steel part.
Vertices are computed in 64-bit and converted with Float32Array.from for the point3f[] attribute, the file format's point type. Loop direction is decided by the sign of the shoelace area and cap triangles by the sign of a 2D cross product, with no tolerance. Segments under 0.01 mm long are skipped.
/** A part's solid: its section loops extruded from x = 0 to its length, split where copes start, in metres. */
function partMesh(p) {
const pr = p.profile, L = p.length, f = p.frame, voff = p.voff || 0;
if (!pr || !pr.loops || !pr.loops.length || !(L > 0)) return null;
const copes = (p.cuts || []).filter((c) => c.type === undefined || c.type === 'cope' || c.length);
const xs = [...new Set([0, L, ...copes.map((c) => (c.end === 'start' ? Math.min(c.length, L) : Math.max(L - c.length, 0)))])].sort((a, c) => a - c);
const P = [], C = [], I = [];
// a mitre's end (0.7.1 71.20): the end ring sits on its cut face
const cutX = (x, u, v) => (p.endCuts?.start && x === 0 ? endX(p, 'start', u, v) : p.endCuts?.end && x === L ? endX(p, 'end', u, v) : x);
const vert = (x, u, v) => { P.push(...M(toWorld(f, cutX(x, u, v), u, v, voff))); return P.length / 3 - 1; };
for (let s = 1; s < xs.length; s++) {
const a = xs[s - 1], z = xs[s];
if (z - a < 0.01) continue;
const mid = (a + z) / 2;
let loops = pr.loops;
for (const c of copes) {
const inCope = c.end === 'start' ? mid < c.length : mid > L - c.length;
if (!inCope) continue;
if (c.side === 'top') loops = loops.map((l, k) => (k === 0 ? clipLoopV(l, pr.depth / 2 - c.depth, true) : null)).filter((l) => l && l.length);
else loops = loops.map((l, k) => (k === 0 ? clipLoopV(l, -pr.depth / 2 + c.depth, false) : null)).filter((l) => l && l.length);
}
if (!loops.length || loops[0].length < 3) continue;
// outer loop counter-clockwise in (u, v), holes clockwise: then (u, v, x) right-handed puts every side face outward
const area = (l) => { let s2 = 0; for (let i = 0; i < l.length; i++) { const [u1, v1] = l[i], [u2, v2] = l[(i + 1) % l.length]; s2 += u1 * v2 - u2 * v1; } return s2 / 2; };
const ordered = loops.map((l, k) => ((k === 0) === (area(l) > 0) ? l : l.slice().reverse()));
for (const l of ordered) {
for (let i = 0; i < l.length; i++) {
const [u1, v1] = l[i], [u2, v2] = l[(i + 1) % l.length];
const q = [vert(a, u1, v1), vert(a, u2, v2), vert(z, u2, v2), vert(z, u1, v1)];
C.push(4); I.push(...q);
}
}
// caps: one triangulation, used at both ends (the near end turned round)
const flat = [], holes = [];
for (const [k, l] of ordered.entries()) { if (k) holes.push(flat.length / 2); for (const [u, v] of l) flat.push(u, v); }
const tris = earcut(flat, holes);
const n = flat.length / 2;
const at = (x) => { const base = P.length / 3; for (let i = 0; i < n; i++) vert(x, flat[i * 2], flat[i * 2 + 1]); return base; };
const b0 = at(a), b1 = at(z);
for (let t = 0; t < tris.length; t += 3) {
// counter-clockwise in (u, v) faces +x (the far end); the near end gets the other turn
let [i0, i1, i2] = [tris[t], tris[t + 1], tris[t + 2]];
const cr = (flat[i1 * 2] - flat[i0 * 2]) * (flat[i2 * 2 + 1] - flat[i0 * 2 + 1]) - (flat[i1 * 2 + 1] - flat[i0 * 2 + 1]) * (flat[i2 * 2] - flat[i0 * 2]);
if (cr < 0) [i1, i2] = [i2, i1];
C.push(3, 3);
I.push(b0 + i0, b0 + i2, b0 + i1);
I.push(b1 + i0, b1 + i1, b1 + i2);
}
}
if (!C.length) return null;
return { points: Float32Array.from(P), counts: C, indices: I };
}