Works out a triangle mesh's volume and area and checks that it is closed and manifold.
How the precision is kept
Volume and area are 64-bit sums over the triangles in index order, from the positions as given (a contour mesh gives 32-bit positions). The volume sums signed tetrahedra to the origin, so a part far from the origin adds large terms that nearly cancel. Topology is decided by counting whole numbers only.
Open item. Use a two-part edge key, as contour.js does, so edges stay distinct above 2²¹ vertices.
Calculations under this policy
3 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.
Each term is the triple product of three corner positions measured from the origin, added in index order and divided by 6 once. For a part far from the origin the terms are large and nearly cancel, so digits are lost as the distance from the origin grows relative to the part's size.
field/measure.js
lines 5 to 13
PartCraft 0.8.1
/** Signed volume (divergence theorem, sum of tetrahedra to the origin), in double precision, in index order. Positive for outward winding. */
export function meshVolume(m) {
const P = m.positions, I = m.indices; let v = 0;
for (let t = 0; t < I.length; t += 3) {
const a = I[t] * 3, b = I[t + 1] * 3, c = I[t + 2] * 3;
v += P[a] * (P[b + 1] * P[c + 2] - P[b + 2] * P[c + 1]) - P[a + 1] * (P[b] * P[c + 2] - P[b + 2] * P[c]) + P[a + 2] * (P[b] * P[c + 1] - P[b + 1] * P[c]);
}
return v / 6;
}
Mesh area
64-bit
The total area of a triangle mesh.
A = Σ |(b − a) × (c − a)| ÷ 2
Edge vectors are differences of corners, so the result does not depend on where the part sits. Triangle areas are added in index order.
field/measure.js
lines 14 to 23
PartCraft 0.8.1
export function meshArea(m) {
const P = m.positions, I = m.indices; let s = 0;
for (let t = 0; t < I.length; t += 3) {
const a = I[t] * 3, b = I[t + 1] * 3, c = I[t + 2] * 3;
const ux = P[b] - P[a], uy = P[b + 1] - P[a + 1], uz = P[b + 2] - P[a + 2], vx = P[c] - P[a], vy = P[c + 1] - P[a + 1], vz = P[c + 2] - P[a + 2];
const x = uy * vz - uz * vy, y = uz * vx - ux * vz, z = ux * vy - uy * vx;
s += Math.sqrt(x * x + y * y + z * z) / 2;
}
return s;
}
Closed and manifold checks
Exact
Counts open, non-manifold and flipped edges, bad vertices, pieces and the Euler number.
Euler number = vertices used − edges + triangles
Every decision is a count of whole numbers; no coordinate is compared. Edges are keyed by lo × 4294967296 + hi, which is exact only while lo is below 2²¹; a mesh with more vertices than that can have two edges share a key.
field/measure.js
lines 24 to 67
PartCraft 0.8.1
/**
* Topology of the mesh as indexed: { closed (every edge in exactly two triangles, one each way), edgeManifold, vertexManifold
* (every vertex's triangles form one fan), boundaryEdges, nonManifoldEdges, nonManifoldVertices, flippedEdges, components,
* degenerate (triangles with a repeated index), euler }. A watertight manifold mesh has all four of the first three true.
*/
export function meshTopology(m) {
const I = m.indices, nv = m.positions.length / 3, nt = I.length / 3;
const edges = new Map();
let degenerate = 0;
for (let t = 0; t < nt; t++) {
const v = [I[t * 3], I[t * 3 + 1], I[t * 3 + 2]];
if (v[0] === v[1] || v[1] === v[2] || v[2] === v[0]) { degenerate++; continue; }
for (let q = 0; q < 3; q++) {
const a = v[q], b = v[(q + 1) % 3], lo = a < b ? a : b, hi = a < b ? b : a, k = lo * 4294967296 + hi;
let e = edges.get(k); if (!e) edges.set(k, (e = { fwd: 0, bwd: 0 }));
if (a < b) e.fwd++; else e.bwd++;
}
}
let boundary = 0, nonManifold = 0, flipped = 0;
for (const e of edges.values()) { const n = e.fwd + e.bwd; if (n === 1) boundary++; else if (n > 2) nonManifold++; else if (e.fwd !== 1) flipped++; }
// vertex fans: triangles around a vertex must connect through shared edges into one piece
const around = new Map();
for (let t = 0; t < nt; t++) for (let q = 0; q < 3; q++) { const v = I[t * 3 + q]; let l = around.get(v); if (!l) around.set(v, (l = [])); l.push(t); }
let badVerts = 0;
for (const [v, ts] of around) {
const parent = new Map(ts.map((t) => [t, t]));
const find = (x) => { while (parent.get(x) !== x) { parent.set(x, parent.get(parent.get(x))); x = parent.get(x); } return x; };
const byNeighbour = new Map();
for (const t of ts) for (let q = 0; q < 3; q++) { const w = I[t * 3 + q]; if (w === v) continue; const l = byNeighbour.get(w); if (l !== undefined) { const a = find(l), b = find(t); if (a !== b) parent.set(a, b); } else byNeighbour.set(w, t); }
const roots = new Set(ts.map(find));
if (roots.size > 1) badVerts++;
}
// connected components over shared vertices
const par = new Int32Array(nv).map((_, i) => i);
const fnd = (x) => { while (par[x] !== x) { par[x] = par[par[x]]; x = par[x]; } return x; };
for (let t = 0; t < nt; t++) { const a = fnd(I[t * 3]), b = fnd(I[t * 3 + 1]), c = fnd(I[t * 3 + 2]); par[b] = a; par[fnd(c)] = fnd(a); }
const comps = new Set(); for (const v of around.keys()) comps.add(fnd(v));
const used = around.size;
return {
closed: boundary === 0 && nonManifold === 0 && flipped === 0 && degenerate === 0, edgeManifold: nonManifold === 0 && flipped === 0,
vertexManifold: badVerts === 0, boundaryEdges: boundary, nonManifoldEdges: nonManifold, nonManifoldVertices: badVerts, flippedEdges: flipped,
components: comps.size, degenerate, euler: used - edges.size + nt,
};
}
In detail
Method
Divergence theorem for the volume; half the cross product's length for the area; edge and vertex-fan counting with union-find for the topology.
Number types
64-bit sums; whole-number edge keys lo × 2³² + hi.
Limits
The edge key is exact only while the smaller vertex index is below 2²¹ (2,097,152); above that two different edges can get the same key.
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.