Resources Numerical accuracy Calculation policy

Mesh volume, area and watertightness checks

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.

field/measure.js PartCraft 0.8.1 64-bit 3 calculations Reviewed

Tests tools/field-test/watertight.mjs

Mesh volume

64-bit

The signed volume enclosed by a triangle mesh.

V = Σ a·(b × c) ÷ 6

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.

Every policy that takes this approach