Resources Numerical accuracy Calculation policy

Face planes, areas, triangles and solid volume

The plane and area of a drawn face, the triangles it is split into, and the surface area and volume of a closed shape.

How the precision is kept

Vertices are 64-bit numbers in meters, kept in the definition's own coordinates. Normals and areas come from Newell sums over a face's corners; volume is a sum of signed tetrahedra from the origin. Both sums are taken about the coordinate origin, so their terms grow with the distance from it while the result does not.

Open item. Take the Newell and tetrahedron sums about a point of the face or solid instead of the origin.

Calculations under this policy

4 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.

m3d/model.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests bin/test-cadcraft-3d.mjs

Plane of a face

64-bit

The plane a face lies in, as a unit normal and a distance from the origin.

n = Newell normal of the outer loop; d = n · p₀

The normal is the normalized Newell sum over the outer loop (newell in vec.js). d is one dot product with the loop's first vertex, so it is as large as the face's distance from the origin, and later plane tests subtract numbers of that size.

  • m3d/model.js
  • lines 277 to 280
  • CadCraft 0.8.2
export function facePlane(def, f) {
  const n = faceNormal(def, f);
  return { n, d: dot(n, def.verts.get(f.loops[0][0])) };
}

Area of a face

64-bit

The area of a face, holes taken out.

area = |Σ (pᵢ × pᵢ₊₁)| ÷ 2 for the outer loop, minus the same for each hole

Each loop's area is half the length of its Newell vector (polyArea3 in vec.js); hole areas are subtracted from the outer one. The Newell terms are products of a coordinate difference and a coordinate sum, so for a face far from the origin they are much larger than the area they add up to.

  • m3d/model.js
  • lines 281 to 285
  • CadCraft 0.8.2
export function faceArea(def, f) {
  let a = polyArea3(f.loops[0].map((v) => def.verts.get(v)));
  for (let i = 1; i < f.loops.length; i++) a -= polyArea3(f.loops[i].map((v) => def.verts.get(v)));
  return a;
}

Triangles of a face

64-bit

The triangles a face is cut into for volume, export and ray tests.

A triangle is kept as it is and a quad is split along a diagonal chosen by a sign test against the face normal, with no tolerance. Other faces are projected to 2D with two dot products per vertex and passed to earcut; each triangle's winding is then fixed with the same sign test.

  • m3d/model.js
  • lines 313 to 342
  • CadCraft 0.8.2
/** Triangles of a face as vertex-id triples (counter-clockwise seen from the front). */
export function faceTris(def, f) {
  const n = faceNormal(def, f);
  if (!len(n)) return [];
  // the common faces of big (imported) models: a triangle is itself, a quad is split along a diagonal that keeps both
  // halves facing front (either one for a convex quad, the one through the dent for a concave one)
  if (f.loops.length === 1 && (f.loops[0].length === 3 || f.loops[0].length === 4)) {
    const l = f.loops[0];
    if (l.length === 3) return [[l[0], l[1], l[2]]];
    const P = l.map((v) => def.verts.get(v));
    const up = (a, b, c) => dot(cross(sub(P[b], P[a]), sub(P[c], P[a])), n) > 0;
    if (up(0, 1, 2) && up(0, 2, 3)) return [[l[0], l[1], l[2]], [l[0], l[2], l[3]]];
    if (up(1, 2, 3) && up(1, 3, 0)) return [[l[1], l[2], l[3]], [l[1], l[3], l[0]]];
  }
  const fr = frame(n);
  const flat = [], ids = [], holes = [];
  f.loops.forEach((l, i) => {
    if (i) holes.push(ids.length);
    for (const v of l) { const p = to2(fr, def.verts.get(v)); flat.push(p[0], p[1]); ids.push(v); }
  });
  const t = earcut(flat, holes);
  const out = [];
  for (let i = 0; i < t.length; i += 3) out.push([ids[t[i]], ids[t[i + 1]], ids[t[i + 2]]]);
  // earcut keeps the input winding of the outer ring only when it was clockwise in its frame;
  // put every triangle on the face's front.
  return out.map((tri) => {
    const [a, b, c] = tri.map((v) => def.verts.get(v));
    return dot(cross(sub(b, a), sub(c, a)), n) < 0 ? [tri[0], tri[2], tri[1]] : tri;
  });
}

Surface area and volume of a shape

64-bit

Whether a group or component is closed, its total face area and, when closed, its volume.

V = Σ a · (b × c) ÷ 6 over all triangles; A = Σ face areas

Volume is a sum of signed tetrahedra from the origin, added in face order; for a shape far from the origin the terms are much larger than the result and cancel. It is returned only when every edge is used by exactly two faces.

  • m3d/model.js
  • lines 2016 to 2026
  • CadCraft 0.8.2
export function solidInfo(def) {
  const ef = edgeFaces(def);
  let closed = def.faces.size > 0;
  for (const fs of ef.values()) if (fs.length !== 2) { closed = false; break; }
  let vol = 0, area = 0;
  for (const f of def.faces.values()) {
    area += faceArea(def, f);
    for (const [a, b, c] of faceTris(def, f)) vol += dot(def.verts.get(a), cross(def.verts.get(b), def.verts.get(c))) / 6;
  }
  return { closed, volume: closed ? vol : null, area, faces: def.faces.size, edges: def.edges.size, verts: def.verts.size };
}

In detail

Method
Newell's method for the normal and area of a polygon; signed tetrahedron sum for volume; ear clipping (the bundled earcut.js) for faces with more than four corners or with holes.
Number types
64-bit numbers in plain arrays [x, y, z].
Tolerances
None in the area and volume sums. The quad split and the triangle winding are decided by a sign test against zero.
Limits
Sums are taken about the origin of the definition: a small face or solid far from that origin loses digits to cancellation. Volume is reported only when every edge has exactly two faces.

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