# BRep and STEP structure

Shape validity, topology counts, STEP units, product structure and assembly occurrences.

Canonical page: https://www.geospec.xyz/docs/reference/brep

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

`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 `GeoSpecAssertionError`. Refusal codes add to the [common refusals](https://www.geospec.xyz/docs/reference/overview.mdx#common-refusals).

**TypeScript**

```ts title="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 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',
});
```

**Python**

```python title="check.py"
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")
```

## toBeValidBrep [#tobevalidbrep]

Asserts that the geometry kernel reports the BRep shape valid, with optional limits on its tolerances and closure.

**Signature**

**TypeScript**

```ts
expectGeo(part).toBeValidBrep(expected?: GeoSpecValidBrepExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_be_valid_brep(closedShells=..., maxTolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| 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_UNSUPPORTED_EVIDENCE`: the subject carries no BRep, or the kernel did not measure a declared limit.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`, naming each limit that failed.

**Examples**

**TypeScript**

```ts
// 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();
```

**Python**

```python
# 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()
```

## toHaveTopologyCounts [#tohavetopologycounts]

Asserts how many vertices, edges, wires, faces, shells, solids and compounds the BRep has.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveTopologyCounts(expected: GeoSpecTopologyCountsExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_topology_counts(faces=..., solids=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| 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_UNSUPPORTED_EVIDENCE`: the subject carries no BRep.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`.

**Examples**

**TypeScript**

```ts
// 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 });
```

**Python**

```python
# 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)
```

## toHaveStepUnits [#tohavestepunits]

Asserts the length unit the STEP file declares.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveStepUnits(expected: GeoSpecStepUnitsExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_step_units(unit=...) -> GeoSpecAssertionReport
```

**Fields**

| 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_UNSUPPORTED_EVIDENCE`: the subject is not a STEP file.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`.

**Examples**

**TypeScript**

```ts
// 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' });
```

**Python**

```python
# 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")
```

## toHaveProductStructure [#tohaveproductstructure]

Asserts the occurrence paths of a STEP assembly or glTF node tree, and their count.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveProductStructure(expected: GeoSpecProductStructureExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_product_structure(names=..., count=...) -> GeoSpecAssertionReport
```

**Fields**

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

**Refusal codes**

* `GEOSPEC_UNSUPPORTED_EVIDENCE` and `GEOSPEC_RESOURCE_LIMIT`: the glTF refusals of `toHaveAssemblyOccurrences`.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`.

**Examples**

**TypeScript**

```ts
// 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 });
```

**Python**

```python
# 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)
```

## toHaveAssemblyOccurrences [#tohaveassemblyoccurrences]

Asserts that named occurrences exist in a STEP assembly or glTF node tree, how many match each name, and where each match sits.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveAssemblyOccurrences(expected: GeoSpecAssemblyOccurrencesExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_assembly_occurrences(occurrences=..., uniqueNames=...) -> GeoSpecAssertionReport
```

**Fields**

| 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_UNSUPPORTED_EVIDENCE`: 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, a `bounds` rule meets an occurrence without finite bounds, or a regular expression uses an unsupported feature.
* `GEOSPEC_RESOURCE_LIMIT`: a glTF file exceeds the configured occurrence or byte limit.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`, listing each rule that failed.

**Examples**

**TypeScript**

```ts
// 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' }] });
```

**Python**

```python
# 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"}])
```