GeoSpec
engine 0.4.0

Check meshes

Claim that a mesh is closed, sound and in one piece, and count the parts of an assembly mesh.

Open Markdown

A mesh answers for its triangles: whether they close, whether any are degenerate or duplicated, and how many pieces they form. Mesh checks run on GLB files; neither loadModel nor load_model admits raw triangles.

1. Claim closure and integrity

const part = await loadModel({ source: 'involute-gear.glb', format: 'glb', sourceUnit: 'mm' });

Tab indents. Press Escape, then Tab, to leave the editor.

The spec as a file, with the model loaded over the native addon
check.ts
import { readFile } from 'node:fs/promises';import { expectGeo } from 'geospec';import { createModelLoader } from 'geospec/model';import { Engine } from 'geospec-engine/node';const engine = new Engine();const loadModel = createModelLoader({ engine, readSource: async (source) => new Uint8Array(await readFile(source as string)) });const part = await loadModel({ source: 'involute-gear.glb', format: 'glb', sourceUnit: 'mm' });expectGeo(part)  .toBeWatertight();expectGeo(part)  .toHaveMeshIntegrity({    finitePositions: true,    degenerateTriangles: { count: 0 },    duplicateFaces: { count: 0 },    triangleCount: 624,  });expectGeo(part)  .toHaveNoDiagnostics();await loadModel.dispose();engine.close();

The engine loads in this tab when you run the spec.

Model
The whole body: this claim measures all of it.

involute-gear.glb · 52 KB · copied from an engine test fixture

Run the spec to see what the engine answers for each claim. Then change an expected value and run it again.

 

The gear is a GLB mesh of 624 triangles. Set triangleCount to 600 and the integrity claim fails with GEOSPEC_MESH_INTEGRITY_MISMATCH, stating 624 triangles, which does not satisfy 600. toBeWatertight reports the edges it checked, { "irregularEdgeFraction": 0, "irregularEdges": 0, "nonManifoldEdges": 0, "openBoundaryEdges": 0, "totalEdges": 936, "watertight": true }, and toHaveNoDiagnostics reads what the loader reported at admission, errors and warnings by default, and measures no geometry.

The measurement is report.evidence.measured on the assertion expectGeo returns.

The measurement is report.result["evidence"]["measured"] on the report expect_geo returns.

Switch the model to the plate, a STEP part: closure passes from the exact BRep topology, and the integrity claim is refused with GEOSPEC_UNSUPPORTED_EVIDENCE, because its fields would measure a tessellation the engine made; toHaveMeshIntegrity({ watertight: true }) is the one form STEP answers.

2. Count the pieces

toHaveConnectedComponents counts closed material shells, and shells closer than toleranceMm count as one. The gear is one piece, so { count: 2 } fails with GEOSPEC_CONNECTED_COMPONENTS_MISMATCH; the count indexes every triangle, which keeps it out of the live demo. The script counts the two 10 mm cubes, 20 mm apart, of an engine fixture, packages/geospec-engine/bench/fixtures/performance-lab/generated/assembly.glb, copied next to it:

check.ts
import { readFile } from 'node:fs/promises';
import { expectGeo } from 'geospec';
import { createModelLoader } from 'geospec/model';
import { Engine } from 'geospec-engine/node';

const engine = new Engine();
const loadModel = createModelLoader({
  engine,
  readSource: async (source) => new Uint8Array(await readFile(source as string)),
});
const cubes = await loadModel({ source: 'assembly.glb', format: 'glb', sourceUnit: 'mm' });

const pieces = expectGeo(cubes).toHaveConnectedComponents({ count: 2 });
const merged = expectGeo(cubes).toHaveConnectedComponents({ count: 1, toleranceMm: 21 });
console.log(pieces.report?.status, merged.report?.status);

await loadModel.dispose();
engine.close();
$ node check.ts
passed passed
check.py
from geospec import GeoSpecEngine, expect_geo, load_model

with GeoSpecEngine(model_root="."):
    cubes = load_model(file="assembly.glb", format="glb", source_unit="mm")
    pieces = expect_geo(cubes).to_have_connected_components(count=2)
    merged = expect_geo(cubes).to_have_connected_components(count=1, toleranceMm=21)
    print(pieces.result["status"], merged.result["status"])
$ python check.py
passed passed

Keyword arguments keep the camelCase field names, as toleranceMm=21 does.

The second claim spans the 20 mm gap with toleranceMm: 21, so the cubes count as one. The tolerance states how close two shells may sit and still be separate parts; it is not a way to make a count pass. Set tolerances and budgets covers it.

Variations

An open mesh. Remove one triangle from a closed box mesh and the claims answer differently: closure fails with GEOSPEC_WATERTIGHT_MISMATCH, The mesh has 3 irregular edges (3 open, 0 non-manifold), so it is not a closed manifold surface.; the component count is refused with GEOSPEC_INVALID_EVIDENCE rather than guessed, because shells are counted over closed, consistently oriented triangles; the integrity claim fails on its triangle count. The open box still reports a volume, 5000 mm³ where the closed one has 6000, which is why closure comes before Check mass properties.

A STEP subject. toHaveConnectedComponents reads the exact BRep topology and passes on the plate with { count: 1 }.

The reference. The mesh reference lists every field of the four claims.

On this page