Groups a mesh's triangles into flat faces and traces each face's outline, for face picking, face profiles in DXF and merged faces in STEP.
How the precision is kept
Triangle normals and plane offsets are 64-bit cross and dot products. Two triangles sharing an edge join one face when their unit normals agree and their plane offsets agree, both within stated tolerances. The offset tolerance scales with the largest coordinate of the mesh.
Calculations under this policy
5 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 unit normal of one triangle and twice its area.
n = (b − a) × (c − a); unit normal = n ÷ |n|
A 64-bit cross product of two edge vectors. A triangle with zero area returns the zero vector instead of dividing by zero.
faces.js
lines 11 to 17
PartCraft 0.8.1
export function triNormal(pos, idx, t) {
const p = (k) => [pos[idx[t * 3 + k] * 3], pos[idx[t * 3 + k] * 3 + 1], pos[idx[t * 3 + k] * 3 + 2]];
const a = p(0), b = p(1), c = p(2);
const n = V.cross(V.sub(b, a), V.sub(c, a));
const l = V.len(n);
return { n: l > 0 ? V.mul(n, 1 / l) : [0, 0, 0], a, area2: l };
}
Coplanar triangle groups
64-bit
Joins triangles that share an edge and lie in one plane.
n₁ · n₂ > 1 − 1e-5 and |d₁ − d₂| < 1e-5 × max(1, max |coordinate|)
The angle test is absolute (about 0.26 degrees between normals); the offset test scales with the largest coordinate in the mesh, with a floor of 1e-5. The group's normal and offset are those of the first triangle that founded it, not an average.
faces.js
lines 41 to 74
PartCraft 0.8.1
/** Coplanar edge-connected triangles of any mesh (ignores B-rep faces; used where facets are wanted). */
export function planarGroupsMesh(mesh, angTol = 1e-5) {
const { positions: pos, indices: idx } = mesh;
const nt = idx.length / 3;
const info = [];
let size = 1;
for (let i = 0; i < pos.length; i++) size = Math.max(size, Math.abs(pos[i]));
const dTol = 1e-5 * size;
for (let t = 0; t < nt; t++) { const q = triNormal(pos, idx, t); info.push({ n: q.n, d: V.dot(q.n, q.a) }); }
const parent = new Int32Array(nt).map((_, i) => i);
const find = (i) => { while (parent[i] !== i) { parent[i] = parent[parent[i]]; i = parent[i]; } return i; };
const edges = new Map();
for (let t = 0; t < nt; t++) {
for (let k = 0; k < 3; k++) {
const a = idx[t * 3 + k], b = idx[t * 3 + (k + 1) % 3];
const key = a < b ? a + ',' + b : b + ',' + a;
const o = edges.get(key);
if (o === undefined) { edges.set(key, t); continue; }
const A = info[t], B = info[o];
if (V.dot(A.n, B.n) > 1 - angTol && Math.abs(A.d - B.d) < dTol) parent[find(t)] = find(o);
}
}
const faceOf = new Int32Array(nt);
const groups = [];
const map = new Map();
for (let t = 0; t < nt; t++) {
const r = find(t);
let g = map.get(r);
if (g === undefined) { g = groups.length; map.set(r, g); groups.push({ tris: [], n: info[t].n, d: info[t].d }); }
groups[g].tris.push(t);
faceOf[t] = g;
}
return { faceOf, groups };
}
Groups from the kernel's faces
64-bit
Uses the kernel's own face of each triangle as the group.
No tolerance is involved in the grouping. The group's normal and plane offset are taken from the first triangle of the face, which is only the whole face's normal when the face is flat.
faces.js
lines 25 to 39
PartCraft 0.8.1
/** The exact kernel's faces as groups (one group per B-rep face; n = its first triangle's normal). */
export function faceGroups(mesh) {
const nt = mesh.indices.length / 3;
const faceOf = new Int32Array(nt);
const byFace = new Map();
const groups = [];
for (let t = 0; t < nt; t++) {
const f = mesh.triFace[t];
let g = byFace.get(f);
if (g === undefined) { const q = triNormal(mesh.positions, mesh.indices, t); g = groups.length; byFace.set(f, g); groups.push({ tris: [], n: q.n, d: V.dot(q.n, q.a), face: f }); }
groups[g].tris.push(t);
faceOf[t] = g;
}
return { groups, faceOf };
}
Face outline and area in its plane
64-bit
The outline loops of a planar face in two dimensions and the face's area.
area = ½ Σ (x_j y_i − x_i y_j)
Vertices are projected into a frame on the face's plane and the shoelace sum runs over those local coordinates. The outer loop and the holes are summed with their signs, so the area is outer minus holes.
faces.js
lines 105 to 113
PartCraft 0.8.1
/** A planar group's loops in its own plane frame (vec.frameFor) — outer first, then holes. */
export function faceLoops2D(mesh, group) {
const pos = mesh.positions;
const frame = V.frameFor(group.n, V.mul(group.n, group.d));
const loops = boundaryLoops(mesh, group.tris).map((l) => l.map((v) => V.toPlane(frame, [pos[v * 3], pos[v * 3 + 1], pos[v * 3 + 2]])));
const area = (l) => { let s = 0; for (let i = 0, j = l.length - 1; i < l.length; j = i++) s += l[j][0] * l[i][1] - l[i][0] * l[j][1]; return s / 2; };
loops.sort((a, b) => Math.abs(area(b)) - Math.abs(area(a)));
return { frame, loops: loops.map((l) => dropCollinear(l)), area: loops.reduce((s, l) => s + area(l), 0) };
}
Dropping collinear outline vertices
64-bit
Removes outline vertices that lie on a straight run.
|u × v| ÷ (|u| |v|) < 1e-6 and u · v > 0
The test is on the sine of the turn angle, so it does not depend on the size of the part. A vertex whose turn is under about 1e-6 radian is removed, which moves the outline by at most that angle times the neighboring edge length.
faces.js
lines 115 to 127
PartCraft 0.8.1
/** Remove vertices that sit on a straight run (mesh boundaries carry them from neighbouring faces). */
export function dropCollinear(loop, sinTol = 1e-6) {
if (loop.length <= 3) return loop;
const out = [];
for (let i = 0; i < loop.length; i++) {
const a = loop[(i + loop.length - 1) % loop.length], b = loop[i], c = loop[(i + 1) % loop.length];
const u = [b[0] - a[0], b[1] - a[1]], v = [c[0] - b[0], c[1] - b[1]];
const l = Math.hypot(...u) * Math.hypot(...v);
const straight = l > 0 && Math.abs(u[0] * v[1] - u[1] * v[0]) / l < sinTol && u[0] * v[0] + u[1] * v[1] > 0;
if (!straight) out.push(b);
}
return out.length >= 3 ? out : loop;
}
In detail
Method
Union-find over shared edges with a normal and offset test; boundary loops from edges used once; shoelace area in the face's own plane.
Number types
64-bit numbers; Int32Array for the triangle to face map. The mesh positions it reads are whatever the kernel gave.
Tolerances
Normals: n₁ · n₂ > 1 − 1e-5 (about 0.26 degrees). Plane offsets: within 1e-5 × the largest absolute coordinate (at least 1). Collinear vertex removal: sine of the turn under 1e-6.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.