# Features

Planar and cylindrical faces, holes, hole patterns, chamfers, fillets, wall thickness and void continuity.

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

These assertions find manufacturing features in a STEP file's exact BRep. A mesh has no surface types, so each refuses on a mesh subject. Every feature claim refuses when the engine cannot classify every face it would need: an unclassified face could be the missing feature, so a pass or a failure would be a guess.

`plate`, `box` and `gear` are the site's demo models; `coupling`, `chamferBlock`, `filletBlock` and `guide` are repository fixtures: six 5 mm holes on a 45 mm bolt circle, one 1 mm planar chamfer, one 2 mm fillet, and a valve guide with a straight bore along z in its `guide` occurrence. Python matchers are snake\_case; their keywords keep the camelCase field names (`holeDiameter=`). A failed or refused call throws `GeoSpecAssertionError` in TypeScript and raises it in Python. 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 box = await loadModel({ source: 'box-10x20x30.step', format: 'step' });
const gear = await loadModel({ source: 'involute-gear.glb', format: 'glb', sourceUnit: 'mm' });
const coupling = await loadModel({
  source: 'packages/geospec-engine/native/occt/rust/tests/fixtures/kernel-skill-acceptance/build123d-coupling.step',
  format: 'step',
});
const chamferBlock = await loadModel({
  source: 'packages/geospec-engine/native/occt/rust/tests/fixtures/edge-treatments/01-planar-chamfer.step',
  format: 'step',
});
const filletBlock = await loadModel({
  source: 'packages/geospec-engine/native/occt/rust/tests/fixtures/edge-treatments/02-cylindrical-fillet.step',
  format: 'step',
});
const guide = await loadModel({
  source: 'packages/geospec-engine/native/occt/rust/tests/fixtures/nominal-bore-void/guide.step',
  format: 'step',
});
```

**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")
    box = load_model(file="box-10x20x30.step", format="step")
    gear = load_model(file="involute-gear.glb", format="glb", source_unit="mm")
    coupling = load_model(
        file="packages/geospec-engine/native/occt/rust/tests/fixtures/kernel-skill-acceptance/build123d-coupling.step",
        format="step",
    )
    chamfer_block = load_model(
        file="packages/geospec-engine/native/occt/rust/tests/fixtures/edge-treatments/01-planar-chamfer.step",
        format="step",
    )
    fillet_block = load_model(
        file="packages/geospec-engine/native/occt/rust/tests/fixtures/edge-treatments/02-cylindrical-fillet.step",
        format="step",
    )
    guide = load_model(
        file="packages/geospec-engine/native/occt/rust/tests/fixtures/nominal-bore-void/guide.step",
        format="step",
    )
```

## toHavePlanarFace [#tohaveplanarface]

Asserts that at least one planar face lies on the declared plane.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHavePlanarFace(expected: GeoSpecPlanarFaceExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_planar_face(normal=..., offset=..., area=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field       | Type                     | Default     | Meaning                                        |
| ----------- | ------------------------ | ----------- | ---------------------------------------------- |
| `normal`    | point                    | required    | Plane normal                                   |
| `offset`    | number, mm               | required    | Signed distance from the origin along `normal` |
| `area`      | numeric expectation, mm² | not checked | Face area                                      |
| `tolerance` | number, 0 or more        | `0.02`      | Error per normal component, offset and area    |

The plane is the set of points `p` with `normal · p = offset`. A face whose outward normal points the other way matches when both the normal and the offset flip.

**Admitted geometry**

* STEP with exact BRep faces whose surface is a plane.

**Refusal codes**

* `GEOSPEC_UNSUPPORTED_EVIDENCE`: the subject carries no BRep.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`, listing every planar face it found.

**Examples**

**TypeScript**

```ts
// passed: plate, its top face is z = 20
expectGeo(plate).toHavePlanarFace({ normal: [0, 0, 1], offset: 20 });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expectGeo(gear).toHavePlanarFace({ normal: [0, 0, 1], offset: 5 });
```

**Python**

```python
# passed: plate, its top face is z = 20
expect_geo(plate).to_have_planar_face(normal=[0, 0, 1], offset=20)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expect_geo(gear).to_have_planar_face(normal=[0, 0, 1], offset=5)
```

## toHaveCylindricalFace [#tohavecylindricalface]

Asserts that at least one cylindrical face has the declared radius along the declared axis.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveCylindricalFace(expected: GeoSpecCylindricalFaceExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_cylindrical_face(radius=..., axis=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field       | Type                  | Default  | Meaning                                            |
| ----------- | --------------------- | -------- | -------------------------------------------------- |
| `radius`    | number, 0 or more, mm | required | Face radius                                        |
| `axis`      | `'x'`, `'y'` or `'z'` | required | Principal axis the cylinder axis points most along |
| `tolerance` | number, 0 or more     | `0.02`   | Error on `radius`                                  |

**Admitted geometry**

* STEP with exact BRep faces whose surface is a cylinder. The face can be a boss or a bore; the claim does not tell them apart.

**Refusal codes**

* `GEOSPEC_UNSUPPORTED_EVIDENCE`: the subject carries no BRep.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`.

**Examples**

**TypeScript**

```ts
// passed: plate, the bore's face has radius 5 along z
expectGeo(plate).toHaveCylindricalFace({ radius: 5, axis: 'z' });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expectGeo(gear).toHaveCylindricalFace({ radius: 2, axis: 'z' });
```

**Python**

```python
# passed: plate, the bore's face has radius 5 along z
expect_geo(plate).to_have_cylindrical_face(radius=5, axis="z")
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expect_geo(gear).to_have_cylindrical_face(radius=2, axis="z")
```

## toHaveCircularHole [#tohavecircularhole]

Asserts that the part has a circular hole of the declared diameter.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveCircularHole(expected: GeoSpecCircularHoleExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_circular_hole(diameter=..., through=..., axis=..., center=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field       | Type                  | Default     | Meaning                                       |
| ----------- | --------------------- | ----------- | --------------------------------------------- |
| `diameter`  | number, 0 or more, mm | required    | Bore diameter                                 |
| `through`   | boolean               | not checked | `true` when both ends open onto outside faces |
| `axis`      | `'x'`, `'y'` or `'z'` | not checked | Bore axis                                     |
| `center`    | point                 | not checked | Centre of the bore's cylindrical face         |
| `tolerance` | number, 0 or more     | `0.02`      | Error on `diameter` and `center`              |

**Admitted geometry**

* STEP with exact BRep faces. A hole is a cylindrical bore the engine qualifies from the solid's topology: material all round it, and each end a mouth, a floor or, opposite a mouth, a counterbore shoulder. A chamfered, countersunk or rounded rim still opens the bore, and its cone or torus counts as part of that mouth. The bore may be one seamed cylinder or a band of coaxial partial cylinders that closes the full turn, which is what a sketched circle drawn as two arcs leaves. It measures diameter, axis, centre and ends, not manufacturability.
* A cross-drilled hole may pierce a bore's wall between its rims, and the bore still counts. Where a cross hole leaves a pin or a hole leaves a slope, its rim is an oblique mouth. A curved floor or a tangent hole stays undecided. An outer chamfer is an exterior cone, and a rounded corner, slot end or flute an open arc, never part of a hole.
* One qualified matching hole passes. A failure needs every bore candidate qualified: an unqualified bore could be the hole, so the claim refuses.

**Refusal codes**

* `GEOSPEC_UNSUPPORTED_EVIDENCE`: the subject carries no BRep, or no qualified hole matches and at least one bore candidate is unqualified.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`: the plate fails `diameter: 12`.

**Examples**

**TypeScript**

```ts
// passed: plate, a 10 mm through bore along z
expectGeo(plate).toHaveCircularHole({ diameter: 10, through: true, axis: 'z' });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expectGeo(gear).toHaveCircularHole({ diameter: 4 });
```

**Python**

```python
# passed: plate, a 10 mm through bore along z
expect_geo(plate).to_have_circular_hole(diameter=10, through=True, axis="z")
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expect_geo(gear).to_have_circular_hole(diameter=4)
```

## toHaveCircularHolePattern [#tohavecircularholepattern]

Asserts that the part has a circular pattern of equal holes.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveCircularHolePattern(expected: GeoSpecCircularHolePatternExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_circular_hole_pattern(count=..., holeDiameter=..., boltCircleDiameter=..., axis=..., center=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field                | Type                  | Default     | Meaning                               |
| -------------------- | --------------------- | ----------- | ------------------------------------- |
| `count`              | integer, 0 or more    | required    | Holes in the pattern, matched exactly |
| `holeDiameter`       | number, 0 or more, mm | required    | Diameter of every hole                |
| `boltCircleDiameter` | number, 0 or more, mm | not checked | Bolt circle diameter                  |
| `axis`               | `'x'`, `'y'` or `'z'` | not checked | Common axis                           |
| `center`             | point                 | not checked | Bolt circle centre                    |
| `tolerance`          | number, 0 or more     | `0.02`      | Error on the diameters and the centre |

A pattern is two or more qualified holes with the same axis, diameter and ends. Holes more than 20 mm apart along the axis start a new pattern.

**Admitted geometry**

* STEP with exact BRep faces, as for `toHaveCircularHole`.
* A pattern needs the complete hole inventory, so any unqualified bore candidate refuses the claim.

**Refusal codes**

* `GEOSPEC_UNSUPPORTED_EVIDENCE`: the subject carries no BRep, or at least one bore candidate is unqualified.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`.

**Examples**

**TypeScript**

```ts
// passed: coupling, six 5 mm holes on a 45 mm bolt circle along z
expectGeo(coupling).toHaveCircularHolePattern({ count: 6, holeDiameter: 5, boltCircleDiameter: 45, axis: 'z' });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expectGeo(gear).toHaveCircularHolePattern({ count: 6, holeDiameter: 5 });
```

**Python**

```python
# passed: coupling, six 5 mm holes on a 45 mm bolt circle along z
expect_geo(coupling).to_have_circular_hole_pattern(count=6, holeDiameter=5, boltCircleDiameter=45, axis="z")
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expect_geo(gear).to_have_circular_hole_pattern(count=6, holeDiameter=5)
```

## toHaveChamferFeature [#tohavechamferfeature]

Asserts that the part has a chamfer of the declared distance.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveChamferFeature(expected: GeoSpecChamferFeatureExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_chamfer_feature(distance=..., selection=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field       | Type                  | Default     | Meaning                                            |
| ----------- | --------------------- | ----------- | -------------------------------------------------- |
| `distance`  | number, 0 or more, mm | required    | Chamfer setback                                    |
| `selection` | string                | any chamfer | The unique label the STEP source gives the chamfer |
| `tolerance` | number, 0 or more     | `0.02`      | Error on `distance`                                |

**Admitted geometry**

* STEP with exact BRep faces. A chamfer is a planar or conical face the engine qualifies between two adjacent faces, with a measured setback.

**Refusal codes**

* `GEOSPEC_UNSUPPORTED_EVIDENCE`: the subject carries no BRep; no qualified chamfer matches and a face's membership is unknown; or the matching chamfer has no unique label for `selection`.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`. Its `details.mismatchKind` separates a wrong label (`selectorMismatch`) from a wrong distance (`geometricMismatch`).

**Examples**

**TypeScript**

```ts
// passed: chamferBlock, one 1 mm planar chamfer
expectGeo(chamferBlock).toHaveChamferFeature({ distance: 1 });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expectGeo(gear).toHaveChamferFeature({ distance: 1 });
```

**Python**

```python
# passed: chamfer_block, one 1 mm planar chamfer
expect_geo(chamfer_block).to_have_chamfer_feature(distance=1)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expect_geo(gear).to_have_chamfer_feature(distance=1)
```

## toHaveFilletFeature [#tohavefilletfeature]

Asserts that the part has a fillet of the declared radius.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveFilletFeature(expected: GeoSpecFilletFeatureExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_fillet_feature(radius=..., selection=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field       | Type                  | Default    | Meaning                                           |
| ----------- | --------------------- | ---------- | ------------------------------------------------- |
| `radius`    | number, 0 or more, mm | required   | Fillet radius                                     |
| `selection` | string                | any fillet | The unique label the STEP source gives the fillet |
| `tolerance` | number, 0 or more     | `0.02`     | Error on `radius`                                 |

**Admitted geometry**

* STEP with exact BRep faces. A fillet is a cylindrical or toroidal face the engine qualifies as tangent to its two neighbours.

**Refusal codes**

* `GEOSPEC_UNSUPPORTED_EVIDENCE`: as for `toHaveChamferFeature`.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`.

**Examples**

**TypeScript**

```ts
// passed: filletBlock, one 2 mm fillet
expectGeo(filletBlock).toHaveFilletFeature({ radius: 2 });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expectGeo(gear).toHaveFilletFeature({ radius: 2 });
```

**Python**

```python
# passed: fillet_block, one 2 mm fillet
expect_geo(fillet_block).to_have_fillet_feature(radius=2)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): gear, a GLB mesh
expect_geo(gear).to_have_fillet_feature(radius=2)
```

## toHaveMinimumWallThickness [#tohaveminimumwallthickness]

Asserts the thinnest wall of the part.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveMinimumWallThickness(expected: GeoSpecMinimumWallThicknessExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_minimum_wall_thickness(value=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field       | Type                    | Default  | Meaning                                                         |
| ----------- | ----------------------- | -------- | --------------------------------------------------------------- |
| `value`     | numeric expectation, mm | required | Thinnest wall; `{ greaterThanOrEqual: 2 }` states a lower limit |
| `tolerance` | number, 0 or more       | `0.02`   | Error on `value`                                                |

**Admitted geometry**

* A STEP part whose whole shape is a complete axis-aligned box or a right circular cylinder. The thickness is the exact minimum over the whole part, not a sample.
* Any other shape refuses, the bored plate included. The engine does not estimate wall thickness from samples or a mesh.

**Refusal codes**

* `GEOSPEC_UNSUPPORTED_EVIDENCE`: the subject carries no BRep, or its shape is outside the qualified box and cylinder domains.

A failed claim carries `GEOSPEC_FEATURE_MISMATCH`.

**Examples**

**TypeScript**

```ts
// passed: box, 10 × 20 × 30 mm, thinnest across x
expectGeo(box).toHaveMinimumWallThickness({ value: 10 });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): plate, a bored plate is outside the box and cylinder domains
expectGeo(plate).toHaveMinimumWallThickness({ value: { greaterThanOrEqual: 2 } });
```

**Python**

```python
# passed: box, 10 × 20 × 30 mm, thinnest across x
expect_geo(box).to_have_minimum_wall_thickness(value=10)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): plate, a bored plate is outside the box and cylinder domains
expect_geo(plate).to_have_minimum_wall_thickness(value={"greaterThanOrEqual": 2})
```

## toHaveVoidContinuity [#tohavevoidcontinuity]

Asserts that an open channel runs through the material along a declared path, such as a bore, duct or passage.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveVoidContinuity(expected: GeoSpecVoidContinuityExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_void_continuity(path=..., material=..., bounds=..., minCrossSection=..., isolatedFrom=...) -> GeoSpecAssertionReport
```

**Fields**

| Field             | Type                                | Default                    | Meaning                                                                        |
| ----------------- | ----------------------------------- | -------------------------- | ------------------------------------------------------------------------------ |
| `path`            | array of points or `{ occurrence }` | required                   | Ordered waypoints in the void; `{ occurrence }` means the centre of its bounds |
| `material`        | array of occurrence names           | required unless `bounds`   | Occurrences whose solids bound the void                                        |
| `bounds`          | `{ min, max }`                      | required unless `material` | Region decided in; without it, the material bounds padded by 2 mm              |
| `minCrossSection` | number, 0 or more, mm²              | not checked                | Smallest cross-section along the path                                          |
| `isolatedFrom`    | array of points                     | `[]`                       | Points the void must not reach                                                 |

Every point must lie inside the region. The published types declare defaults for `material` and `bounds`; the engine refuses a claim with neither. The path's shape picks the route:

* Points and `bounds`: the engine follows the void through a complete box or right circular cylinder. Elsewhere, each straight segment must clear the material: grazing or `isolatedFrom` refuses, and a point in material fails.
* Points without `bounds`: the path must run along one straight bore in exactly one `material` occurrence, with 2 to 16 points and no `isolatedFrom`. This route passes or refuses; it never fails.
* Any `{ occurrence }` waypoint: the engine decides the path on an exact solid model of the material. `minCrossSection` refuses here, because the sections are sampled.

**Admitted geometry**

* STEP with exact BRep faces and the named material occurrences.
* A path the engine cannot decide refuses. An unresolved section does not show a blocked channel.

**Refusal codes**

* `GEOSPEC_VOID_CONTINUITY_UNSUPPORTED`: neither `material` nor `bounds`, an absent occurrence, a point outside the region, or a path outside the route's limits.
* `GEOSPEC_UNSUPPORTED_EVIDENCE`: `minCrossSection` on the occurrence route, the subject carries no BRep, a material is not a complete solid, or the occurrence route has no exact solid model.
* `GEOSPEC_ANALYSIS_LIMIT`: the arrangement needs more than a capacity the proof profile fixes, such as 16 distinct materials, or a cell, record or exact-arithmetic capacity. `details` carry `limit` and `required`, and no host limit raises them.

A failed claim carries `GEOSPEC_VOID_CONTINUITY_MISMATCH`.

**Examples**

**TypeScript**

```ts
// passed: guide, a straight bore along z through the 'guide' occurrence
expectGeo(guide).toHaveVoidContinuity({
  material: ['guide'],
  path: [
    [0, 0, 3],
    [0, 0, 42],
  ],
});
// refused (GEOSPEC_VOID_CONTINUITY_UNSUPPORTED): gear, a GLB mesh has no 'guide' occurrence
expectGeo(gear).toHaveVoidContinuity({
  material: ['guide'],
  path: [
    [0, 0, 3],
    [0, 0, 42],
  ],
});
```

**Python**

```python
# passed: guide, a straight bore along z through the 'guide' occurrence
expect_geo(guide).to_have_void_continuity(material=["guide"], path=[[0, 0, 3], [0, 0, 42]])
# refused (GEOSPEC_VOID_CONTINUITY_UNSUPPORTED): gear, a GLB mesh has no 'guide' occurrence
expect_geo(gear).to_have_void_continuity(material=["guide"], path=[[0, 0, 3], [0, 0, 42]])
```