BRep and STEP structure
Shape validity, topology counts, STEP units, product structure and assembly occurrences.
These assertions read a STEP file's exact BRep and assembly structure; the occurrence assertions also read glTF nodes. A mesh has no BRep, so BRep assertions refuse on it with GEOSPEC_.
plate and gear are the site's demo models; inchCube, cubes and deepNodes are repository fixtures: a STEP cube authored in inches, two 10 mm cubes, cubeA at the origin and cubeB centred at x = 30, and a GLB 66 nodes deep. Python matchers are snake_case; their keywords keep the camelCase field names (toleranceMm=). A failed or refused call raises Geo. Refusal codes add to the common refusals.
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 plate = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });
const gear = await loadModel({ source: 'involute-gear.glb', format: 'glb', sourceUnit: 'mm' });
const inchCube = await loadModel({
source: 'packages/geospec-engine/native/occt/rust/tests/fixtures/inch-cube.step',
format: 'step',
});
const cubes = await loadModel({
source: 'packages/geospec-engine/native/occt/rust/tests/fixtures/two-cube-assembly.step',
format: 'step',
});
const deepNodes = await loadModel({
source: 'packages/geospec-engine/rust/tests/fixtures/gltf-occurrences/deep-nodes.glb',
format: 'glb',
sourceUnit: 'mm',
});from geospec import GeoSpecEngine, expect_geo, load_model
with GeoSpecEngine(model_root="."):
plate = load_model(file="plate-with-through-bore.step", format="step")
gear = load_model(file="involute-gear.glb", format="glb", source_unit="mm")
inch_cube = load_model(file="packages/geospec-engine/native/occt/rust/tests/fixtures/inch-cube.step", format="step")
cubes = load_model(file="packages/geospec-engine/native/occt/rust/tests/fixtures/two-cube-assembly.step", format="step")
deep_nodes = load_model(file="packages/geospec-engine/rust/tests/fixtures/gltf-occurrences/deep-nodes.glb", format="glb", source_unit="mm")to Be Valid Brep
Asserts that the geometry kernel reports the BRep shape valid, with optional limits on its tolerances and closure.
Signature
expectGeo(part).toBeValidBrep(expected?: GeoSpecValidBrepExpectation): GeoSpecAssertionexpect_geo(part).to_be_valid_brep(closedShells=..., maxTolerance=...) -> GeoSpecAssertionReportFields
| Field | Type | Default | Meaning |
|---|---|---|---|
maxTolerance | number, 0 or more | not checked | Shape tolerance, at most |
freeBounds | { count?: numeric expectation } | not checked | Free boundary edges |
minEdgeLength | number, 0 or more | not checked | Edge length, at least |
sameParameter | boolean | not checked | Edges agree with their surface parameters |
closedShells, closedWires | boolean | not checked | Every shell, or every wire, is closed |
Admitted geometry
- STEP with exact BRep faces.
- A declared limit the kernel did not measure refuses rather than passing.
Refusal codes
GEOSPEC_: the subject carries no BRep, or the kernel did not measure a declared limit.UNSUPPORTED_ EVIDENCE
A failed claim carries GEOSPEC_, naming each limit that failed.
Examples
// passed: plate, closed shells, shape tolerance 1e-7 mm
expectGeo(plate).toBeValidBrep({ closedShells: true, maxTolerance: 0.01 });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a mesh carries no BRep
expectGeo(gear).toBeValidBrep();# passed: plate, closed shells, shape tolerance 1e-7 mm
expect_geo(plate).to_be_valid_brep(closedShells=True, maxTolerance=0.01)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a mesh carries no BRep
expect_geo(gear).to_be_valid_brep()to Have Topology Counts
Asserts how many vertices, edges, wires, faces, shells, solids and compounds the BRep has.
Signature
expectGeo(part).toHaveTopologyCounts(expected: GeoSpecTopologyCountsExpectation): GeoSpecAssertionexpect_geo(part).to_have_topology_counts(faces=..., solids=..., tolerance=...) -> GeoSpecAssertionReportFields
| Field | Type | Default | Meaning |
|---|---|---|---|
vertices, edges, wires, faces, shells, solids, compounds | numeric expectation | not checked | The kernel's counts |
tolerance | number, 0 or more | 0 | Allowed difference per count |
Admitted geometry
- STEP with exact BRep faces.
- The counts are the kernel's own, by occurrence: a vertex or edge shared by several faces counts once per use, so the plate reports 60 vertices and 30 edges for its 7 faces, six sides and the bore. Assert faces and solids when you mean the part's shape.
Refusal codes
GEOSPEC_: the subject carries no BRep.UNSUPPORTED_ EVIDENCE
A failed claim carries GEOSPEC_.
Examples
// passed: plate, six sides and the bore in one solid
expectGeo(plate).toHaveTopologyCounts({ faces: 7, solids: 1 });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a mesh has no BRep topology
expectGeo(gear).toHaveTopologyCounts({ faces: 6 });# passed: plate, six sides and the bore in one solid
expect_geo(plate).to_have_topology_counts(faces=7, solids=1)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a mesh has no BRep topology
expect_geo(gear).to_have_topology_counts(faces=6)to Have Step Units
Asserts the length unit the STEP file declares.
Signature
expectGeo(part).toHaveStepUnits(expected: GeoSpecStepUnitsExpectation): GeoSpecAssertionexpect_geo(part).to_have_step_units(unit=...) -> GeoSpecAssertionReportFields
| Field | Type | Default | Meaning |
|---|---|---|---|
unit | 'mm', 'cm', 'm', 'in' or 'ft' | required | Compared as a literal string; TypeScript types it as string |
Admitted geometry
- Any STEP subject, including one whose faces carry no surface: the claim reads the declared unit, not faces. The engine still measures in millimetres.
Refusal codes
GEOSPEC_: the subject is not a STEP file.UNSUPPORTED_ EVIDENCE
A failed claim carries GEOSPEC_.
Examples
// passed: inchCube, declared in inches
expectGeo(inchCube).toHaveStepUnits({ unit: 'in' });
// failed (GEOSPEC_FEATURE_MISMATCH): inchCube, the file declares 'in', not 'inch'
expectGeo(inchCube).toHaveStepUnits({ unit: 'inch' });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a mesh declares no unit
expectGeo(gear).toHaveStepUnits({ unit: 'mm' });# passed: inch_cube, declared in inches
expect_geo(inch_cube).to_have_step_units(unit="in")
# failed (GEOSPEC_FEATURE_MISMATCH): inch_cube, the file declares 'in', not 'inch'
expect_geo(inch_cube).to_have_step_units(unit="inch")
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a mesh declares no unit
expect_geo(gear).to_have_step_units(unit="mm")to Have Product Structure
Asserts the occurrence paths of a STEP assembly or glTF node tree, and their count.
Signature
expectGeo(part).toHaveProductStructure(expected: GeoSpecProductStructureExpectation): GeoSpecAssertionexpect_geo(part).to_have_product_structure(names=..., count=...) -> GeoSpecAssertionReportFields
| Field | Type | Default | Meaning |
|---|---|---|---|
names | array of strings | [] | Occurrence paths that must exist |
count | numeric expectation | not checked | Occurrences in the file; the single-part plate passes count: 0 |
Admitted geometry
- Any STEP subject; the claim reads the assembly structure, not faces.
- glTF/GLB: the node occurrences of
to.Have Assembly Occurrences
Refusal codes
GEOSPEC_andUNSUPPORTED_ EVIDENCE GEOSPEC_: the glTF refusals ofRESOURCE_ LIMIT to.Have Assembly Occurrences
A failed claim carries GEOSPEC_.
Examples
// passed: cubes, occurrences cubeA and cubeB
expectGeo(cubes).toHaveProductStructure({ names: ['cubeA', 'cubeB'], count: 2 });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): deepNodes, 66 levels of nodes
expectGeo(deepNodes).toHaveProductStructure({ count: 1 });# passed: cubes, occurrences cubeA and cubeB
expect_geo(cubes).to_have_product_structure(names=["cubeA", "cubeB"], count=2)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): deep_nodes, 66 levels of nodes
expect_geo(deep_nodes).to_have_product_structure(count=1)to Have Assembly Occurrences
Asserts that named occurrences exist in a STEP assembly or glTF node tree, how many match each name, and where each match sits.
Signature
expectGeo(part).toHaveAssemblyOccurrences(expected: GeoSpecAssemblyOccurrencesExpectation): GeoSpecAssertionexpect_geo(part).to_have_assembly_occurrences(occurrences=..., uniqueNames=...) -> GeoSpecAssertionReportFields
| Field | Type | Default | Meaning |
|---|---|---|---|
occurrences[].name | string or regular expression | required | Path to match; GeoSpecRegex in Python |
occurrences[].count | numeric expectation | at least one match | Matches required; a rule that matches none fails |
occurrences[].bounds | { min?, max?, center?, within?, tolerance? } | not checked | Checked on every match; tolerance is 0.02 per axis |
uniqueNames | boolean | false | Fail when an occurrence path repeats |
min, max and center compare each match's box axis by axis. within names another occurrence by path or regular expression whose box, widened by tolerance on every face, must contain each match; an occurrence never contains itself.
Admitted geometry
- A STEP file with assembly occurrences. A file with none refuses rather than reporting zero matches.
- glTF/GLB: each node with triangles under it, its path built as STEP paths are and its box spanning those triangles.
- A regular expression feature the engine cannot evaluate faithfully refuses the claim; a malformed one refuses with
GEOSPEC_.INVALID_ EXPECTATION
Refusal codes
GEOSPEC_: the subject is a bare mesh, the file has no assembly occurrences, a glTF tree is over 64 levels deep or has a non-finite vertex, aUNSUPPORTED_ EVIDENCE boundsrule meets an occurrence without finite bounds, or a regular expression uses an unsupported feature.GEOSPEC_: a glTF file exceeds the configured occurrence or byte limit.RESOURCE_ LIMIT
A failed claim carries GEOSPEC_, listing each rule that failed.
Examples
// passed: cubes, one cubeA; cubeB spans x 25 to 35
expectGeo(cubes).toHaveAssemblyOccurrences({
occurrences: [
{ name: 'cubeA', count: 1 },
{ name: 'cubeB', bounds: { min: [25, -5, -5], max: [35, 5, 5] } },
],
uniqueNames: true,
});
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): plate, a single part has no occurrences
expectGeo(plate).toHaveAssemblyOccurrences({ occurrences: [{ name: 'cubeA' }] });# passed: cubes, one cubeA; cubeB spans x 25 to 35
expect_geo(cubes).to_have_assembly_occurrences(
occurrences=[{"name": "cubeA", "count": 1}, {"name": "cubeB", "bounds": {"min": [25, -5, -5], "max": [35, 5, 5]}}],
uniqueNames=True,
)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): plate, a single part has no occurrences
expect_geo(plate).to_have_assembly_occurrences(occurrences=[{"name": "cubeA"}])