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