Produces the triangle mesh of the exact solid used for the view, STL and 3MF export and assembly checks, and reads its volume, surface area, face areas and edge lengths from the kernel.
How the precision is kept
Volume, area, face areas, centers and edge lengths come from OpenCascade's property integrators as 64-bit numbers. The mesh is read back from the WebAssembly memory as 32-bit floats and stays in a Float32Array; it feeds the view, the STL and 3MF exports and the assembly interference check. Mesh density is set by a chord tolerance of 2e-4 of the bounding-box diagonal and an angle step.
Open item. Keep a 64-bit copy of the mesh positions for the STL, 3MF and interference paths, and use the adaptive integration for the surface area of bodies with spline faces as is done for the volume.
Calculations under this policy
9 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.
brep.jsPartCraft 0.8.1 32-bit in part9 calculationsReviewed
Tests bin/test-partcraft.mjs
Mesh tolerances
64-bit
How closely the triangle mesh follows the exact faces.
The chord tolerance is relative to the part's size, so the mesh of a 100 mm part may sit up to about 0.02 mm from a curved face. Everything measured on the mesh inherits this.
Copies the mesh's vertices, normals and triangles out of the kernel's memory.
vertices and normals: Float32Array view of the WebAssembly buffer
The vertex coordinates are read as 32-bit floats from the WebAssembly memory. Every later use of mesh.positions (STL, 3MF, interference, draft areas) starts from these 32-bit values.
brep.js
lines 1052 to 1056
PartCraft 0.8.1
const md = T(oc.ReplicadMeshExtractor.extract(body, lin, ang, false));
const mem = () => oc.wasmMemory.buffer;
const vs = new Float32Array(mem(), md.getVerticesPtr(), md.getVerticesSize()).slice();
const ns = new Float32Array(mem(), md.getNormalsPtr(), md.getNormalsSize()).slice();
const ts = new Int32Array(mem(), md.getTrianglesPtr(), md.getTrianglesSize()).slice();
Welding the mesh
32-bit in part
Merges vertices that different faces share so the mesh is watertight.
same vertex when x, y and z are identical
Vertices are matched by the text of their three 32-bit coordinates, so only bit-identical positions merge; there is no distance tolerance. The comment states that the kernel discretizes a shared edge once, which is what makes exact matching work. Welded positions are stored in a Float32Array.
brep.js
lines 1070 to 1080
PartCraft 0.8.1
// weld by exact position (OCCT discretises a shared edge once, so both faces get the same points)
const nv = vs.length / 3;
const remap = new Int32Array(nv);
const key = new Map();
const pos = [];
for (let i = 0; i < nv; i++) {
const k = vs[i * 3] + ',' + vs[i * 3 + 1] + ',' + vs[i * 3 + 2];
let w = key.get(k);
if (w === undefined) { w = pos.length / 3; key.set(k, w); pos.push(vs[i * 3], vs[i * 3 + 1], vs[i * 3 + 2]); }
remap[i] = w;
}
Volume and surface area
64-bit
The solid's volume and total surface area, from the exact faces.
V = ∭ dV (Gauss–Kronrod, tolerance 1e-12, when a non-line, non-circle edge exists); A = ∬ dA
Both values come from OpenCascade as 64-bit numbers and do not use the mesh. The adaptive integration is chosen by edge type: a body whose edges are all lines and circles uses the default volume integration. The area call is the default one in every case.
brep.js
lines 1124 to 1130
PartCraft 0.8.1
// exact properties
const gv = T(new oc.GProp_GProps());
// spline edges (sketch splines): OCCT's default volume integration is ~1e-4 off there; the adaptive Gauss–Kronrod one is exact to ~1e-12
if (edgeRecs.some((e) => e.type === 'curve')) oc.BRepGProp.VolumePropertiesGK(body, gv, 1e-12, false, true, false, false, false);
else oc.BRepGProp.VolumeProperties(body, gv, false, false, false);
const ga = T(new oc.GProp_GProps());
oc.BRepGProp.SurfaceProperties(body, ga, false, false);
Genus
64-bit
The number of through holes (handles) of the solid, from the welded mesh.
χ = V − E + F; genus = round(bodies − χ ÷ 2)
Integer counts. Each mesh edge is keyed as a × 4294967296 + b, which is exact in a 64-bit number only while a is below 2²¹ (2,097,152 welded vertices); above that, different edges can share a key and the count goes wrong.
brep.js
lines 1132 to 1139
PartCraft 0.8.1
// genus from the welded mesh: χ = V − E + F, genus = bodies − χ/2
const ek = new Set();
for (let t = 0; t < indices.length / 3; t++) for (let k = 0; k < 3; k++) { const a = indices[t * 3 + k], b = indices[t * 3 + (k + 1) % 3]; ek.add(a < b ? a * 4294967296 + b : b * 4294967296 + a); }
const chi = positions.length / 3 - ek.size + indices.length / 3;
const stats = {
volume: gv.Mass(), area: ga.Mass(), genus: Math.round(bodies - chi / 2), tris: indices.length / 3, bodies,
bbox: bb, faces: table.length, edges: edgeRecs.length, exact: true,
};
Face area and center
64-bit
Each face's area and center point, listed with the face.
area = ∬ dA over the face; center = its centroid
Read from OpenCascade's surface properties with its default integration, as 64-bit numbers. The center is also the point stored (rounded to 6 decimals) when the face is picked.
The length is OpenCascade's linear property of the edge. The middle point is at the middle of the parameter range, which is the middle by length only for lines, circles and other uniformly parameterized curves.
brep.js
lines 894 to 899
PartCraft 0.8.1
const t0 = c.FirstParameter(), t1 = c.LastParameter();
const a = H.xyz(T(c.EvalD0(t0))), b = H.xyz(T(c.EvalD0(t1))), m = H.xyz(T(c.EvalD0((t0 + t1) / 2)));
const type = c.GetType();
const gp = T(new oc.GProp_GProps());
oc.BRepGProp.LinearProperties(x.edge, gp, false, false);
const rec = { edge: x.edge, faces: [...x.faces].sort(), key: [...x.faces].sort().join('|'), at: m, a, b, len: gp.Mass(), type: type === 'GeomAbs_Line' ? 'line' : type === 'GeomAbs_Circle' ? 'circle' : 'curve' };
Bounding box
64-bit
The smallest axis-aligned box around a shape.
min, max per axis from the kernel's optimal box
Uses OpenCascade's BRepBndLib.AddOptimal with both of its flags set to false, and returns the six limits as 64-bit numbers.
The solid count, face count, volume and bounding box shown when a STEP file is imported.
volume = ∭ dV (default integration)
The volume here always uses OpenCascade's default integration, not the adaptive one used for the part's own statistics, so for a file with spline edges the two figures can differ.
brep.js
lines 1182 to 1195
PartCraft 0.8.1
/** Facts about STEP text read back by OCCT (for the import dialog and the tests). */
export function stepInfo(oc, text) {
const T = makeArena();
const H = helpers(oc, T);
try {
const shape = readStepShape(oc, text, { cache: false });
T(shape);
const g = T(new oc.GProp_GProps());
oc.BRepGProp.VolumeProperties(shape, g, false, false, false);
const surf = {};
for (const f of H.unique(shape, 'FACE')) { const t = T(new oc.BRepAdaptor_Surface(T(oc.TopoDS.Face(f)), true)).GetType().replace('GeomAbs_', ''); surf[t] = (surf[t] || 0) + 1; }
return { solids: H.explore(shape, 'SOLID').length, faces: H.unique(shape, 'FACE').length, volume: g.Mass(), surfaces: surf, bbox: bboxOf(oc, T, shape) };
} finally { T.free(); }
}
In detail
Method
Mesh by the kernel's extractor with linear deflection diagonal × 2e-4 and angular deflection 2π ÷ max(16, resolution, default 64). Volume by adaptive Gauss–Kronrod integration with tolerance 1e-12 when the body has an edge that is neither a line nor a circle, otherwise OpenCascade's default volume integration. Area by its default surface integration.
Number types
Float32Array for mesh positions, normals and edge polylines; Int32Array and Uint32Array for indexes; 64-bit numbers for volume, area, lengths and the bounding box.
Tolerances
Linear deflection 2e-4 × diagonal (diagonal at least 1e-6). Gauss–Kronrod tolerance 1e-12. Vertices are welded only when their three 32-bit coordinates are identical.
Limits
A comment in the code says the default volume integration is about 1e-4 off on spline edges, which is why the adaptive one is used there; the surface area always uses the default integration. The genus count packs two vertex numbers into one 64-bit number and is exact only below 2,097,152 vertices.
Approach: 32-bit step, flagged
One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.