# Relationships and interference

Component interference and spatial relationships between selected parts, faces and bodies.

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

Each route here is narrow: a pair the engine has no exact method for refuses, and never falls back to an estimate.

`assembly` and `gapBoxes` are repository fixtures: two 10 mm cubes as GLB nodes `cubeA` and `cubeB`, 20 mm apart, and two 10 mm STEP boxes named `subject` and `target`, 2 mm apart along x. Python matchers are snake\_case; their keywords keep the camelCase field names (`maxVolume=`). 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 assembly = await loadModel({
  source: 'packages/geospec-engine/bench/fixtures/performance-lab/generated/assembly.glb',
  format: 'glb',
  sourceUnit: 'mm',
});
const gapBoxes = await loadModel({
  source: 'packages/geospec-engine/native/occt/rust/tests/fixtures/box-clearance/two-mm.step',
  format: 'step',
});
```

**Python**

```python title="check.py"
from geospec import GeoSpecEngine, expect_geo, load_model

with GeoSpecEngine(model_root="."):
    assembly = load_model(
        file="packages/geospec-engine/bench/fixtures/performance-lab/generated/assembly.glb",
        format="glb",
        source_unit="mm",
    )
    gap_boxes = load_model(
        file="packages/geospec-engine/native/occt/rust/tests/fixtures/box-clearance/two-mm.step",
        format="step",
    )
```

## toHaveNoComponentInterference [#tohavenocomponentinterference]

Asserts that no two components of an assembly share material beyond a declared allowance.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveNoComponentInterference(expected?: GeoSpecComponentInterferenceExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_no_component_interference(tolerance=..., pairs=..., allowances=...) -> GeoSpecAssertionReport
```

**Fields**

| Field        | Type                                                  | Default    | Meaning                                                                                      |
| ------------ | ----------------------------------------------------- | ---------- | -------------------------------------------------------------------------------------------- |
| `tolerance`  | number, 0 or more                                     | `0.001`    | Recorded in the evidence; the volume comparison is exact                                     |
| `pairs`      | array of `{ left, right }`                            | every pair | Pairs to check                                                                               |
| `allowances` | array of `{ left, right, reason, maxVolume?, kind? }` | `[]`       | Pairs allowed up to `maxVolume` mm³; without `maxVolume` the pair is waived and not measured |

A component selector is an exact label or a `RegExp` (`GeoSpecRegex` in Python); `kind` is `'intentionalInterference'`.

**Admitted geometry**

* Two or more components. The components are the leaf occurrences with faces in a STEP file, or the named nodes of a glTF or GLB file, labelled `node#primitive` (`cubeA#0`). A `mesh-buffer-v1` mesh has one primitive and no components.
* glTF/GLB: each pair's shared volume is computed exactly over closed, consistently oriented shells and compared with `maxVolume`, or with zero.
* STEP: a pair of solids whose faces stay more than twice the larger shape tolerance apart, neither inside the other, shares no material. A touching or overlapping pair needs an allowance; a `maxVolume` allowance is decided only for a bore-and-shaft pair (a cylindrical bore through a slab, and a finite cylinder) by bounding the shared volume from above. A STEP claim passes or refuses, never fails.

**Refusal codes**

* `GEOSPEC_COMPONENT_PARTITION_INCONCLUSIVE`: fewer than two components.
* `GEOSPEC_SELECTOR_UNMATCHED`: a `pairs` entry matches no pair of distinct components.
* `GEOSPEC_UNSUPPORTED_EVIDENCE`: a STEP pair is not shown apart and has no allowance, or its `maxVolume` allowance is not decided.
* `GEOSPEC_INVALID_EVIDENCE`: a mesh component is not a closed, consistently oriented shell.
* `GEOSPEC_UNSUPPORTED_SELECTOR`: a `RegExp` with the `v` flag, outside the engine's regex profile.
* `GEOSPEC_ANALYSIS_LIMIT`: a bore-and-shaft bound needs more refinement or exact arithmetic than its proof profile fixes, which no host limit raises, or a mesh pair's exact intersection needs more scratch than `maxMeshBytes` allows.

A failed claim carries `GEOSPEC_COMPONENT_INTERFERENCE`.

**Examples**

**TypeScript**

```ts
// passed: assembly, cubeA#0 and cubeB#0 share no material
expectGeo(assembly).toHaveNoComponentInterference();
// passed: gapBoxes, the STEP boxes sit 2 mm apart
expectGeo(gapBoxes).toHaveNoComponentInterference();
// refused (GEOSPEC_SELECTOR_UNMATCHED): assembly has no component cubeC#0
expectGeo(assembly).toHaveNoComponentInterference({ pairs: [{ left: 'cubeA#0', right: 'cubeC#0' }] });
```

**Python**

```python
# passed: assembly, cubeA#0 and cubeB#0 share no material
expect_geo(assembly).to_have_no_component_interference()
# passed: gap_boxes, the STEP boxes sit 2 mm apart
expect_geo(gap_boxes).to_have_no_component_interference()
# refused (GEOSPEC_SELECTOR_UNMATCHED): assembly has no component cubeC#0
expect_geo(assembly).to_have_no_component_interference(pairs=[{"left": "cubeA#0", "right": "cubeC#0"}])
```

## toHaveSpatialRelationships [#tohavespatialrelationships]

Asserts declared relationships between selected parts, faces or bodies.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveSpatialRelationships(expected: GeoSpecSpatialRelationshipsExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_spatial_relationships(relationships=[...]) -> GeoSpecAssertionReport
```

**Fields**

Each `relationships` entry:

| Field                     | Type                  | Default                       | Meaning                                                                                                                                         |
| ------------------------- | --------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind`                    | relationship kind     | required                      | `contact`, `clearance`, `coaxial`, `concentric`, `coplanar`, `parallel`, `perpendicular`, `angle`, `containment`, `insertion` or `interference` |
| `subject`, `target`       | selector              | required                      | The two endpoints                                                                                                                               |
| `tolerance`               | number, 0 or more, mm | `0.02`; `0` for `clearance`   | Allowed distance error                                                                                                                          |
| `angularToleranceDegrees` | number, 0 or more     | `0.5`                         | Allowed direction error                                                                                                                         |
| `angleDegrees`            | number                | `0`; `90` for `perpendicular` | Expected angle for `angle`, `parallel` and `perpendicular`                                                                                      |
| `axis`                    | `[x, y, z]`           | none                          | Insertion axis                                                                                                                                  |
| `min`, `max`              | number, mm            | `min` `0` for `insertion`     | Band of clearance or insertion depth                                                                                                            |
| `minVolume`, `maxVolume`  | number, mm³           | `0`                           | Band of shared volume for `interference`                                                                                                        |
| `id`, `reason`            | string                | none                          | Labels carried into the evidence                                                                                                                |

A selector is an occurrence path string, a `RegExp` (`GeoSpecRegex` in Python) over occurrence names, or a catalog object with `kind` (`occurrence`, `body`, `face`, `axis`, `plane`, `datum`, `interface` or `group`), `of`, `query` and `expect`. The engine runs the catalog forms: `{ kind: 'axis', query: { axis: { direction: [0, 0, 1] } } }` on both sides passes `coaxial` on the demo plate. The published types give `axis` and `plane` explicit `axis`, `center`, `direction`, `radius`, `normal` and `offset` members instead, which the engine refuses as unknown fields (`GEOSPEC_INVALID_EXPECTATION`); `tsc` reports an excess property on the forms that run, and `as GeoSpecGeometrySelector` compiles them. Strings, `RegExp`, `body` and `face` selectors compile and run as written.

**Admitted geometry**

* STEP, by kind: `clearance`, `contact`, `insertion` and `interference` take two complete box-shaped occurrences, and `insertion` a Cartesian `axis`; `coaxial`, `concentric`, `coplanar`, `parallel`, `perpendicular` and `angle` take one analytic face, axis or plane per endpoint; `containment` takes closed solids. Clearance must lie in `[min - tolerance, max + tolerance]`, shared volume in `[minVolume, maxVolume]`.
* glTF/GLB bodies (`{ kind: 'body', of: 'cubeA#0' }`) admit `contact`, `clearance`, `containment` and `interference` over closed shells; the other kinds refuse on a mesh.

**Refusal codes**

* `GEOSPEC_SELECTOR_UNMATCHED`, `GEOSPEC_SELECTOR_AMBIGUOUS`: a catalog selector or `RegExp` resolves to no entity, or to more than its cardinality allows.
* `GEOSPEC_SELECTOR_UNSUPPORTED_EVIDENCE`: an endpoint outside the kind's shapes (a whole occurrence for `parallel`), a mesh endpoint that is not a body, or a bare string naming no occurrence, which the engine reads as an interface name the file does not carry.
* `GEOSPEC_UNSUPPORTED_EVIDENCE`: an insertion outside the route above, such as boxes without `axis`, or geometry outside a proof's domain, named in `details.applicability.condition`.
* `GEOSPEC_INVALID_EVIDENCE`: a mesh body that is not a closed, consistently oriented shell.
* `GEOSPEC_UNSUPPORTED_SELECTOR`: a `RegExp` with the `v` flag.
* `GEOSPEC_ANALYSIS_LIMIT`: an insertion, clearance, cylindrical band or finite contact proof needs more than a capacity its profile fixes, such as 64 faces on a contact target. `details` carry `limit` and `required`, and no host limit raises them.

A failed claim carries `GEOSPEC_SPATIAL_RELATIONSHIP_MISMATCH`.

**Examples**

**TypeScript**

```ts
// passed: gapBoxes, the boxes sit 2 mm apart in x
expectGeo(gapBoxes).toHaveSpatialRelationships({
  relationships: [{ kind: 'clearance', subject: 'subject', target: 'target', min: 2 }],
});
// refused (GEOSPEC_SELECTOR_UNSUPPORTED_EVIDENCE): gapBoxes, parallel needs a face, axis or plane per side
expectGeo(gapBoxes).toHaveSpatialRelationships({
  relationships: [{ kind: 'parallel', subject: 'subject', target: 'target' }],
});
```

**Python**

```python
# passed: gap_boxes, the boxes sit 2 mm apart in x
expect_geo(gap_boxes).to_have_spatial_relationships(
    relationships=[dict(kind="clearance", subject="subject", target="target", min=2)],
)
# refused (GEOSPEC_SELECTOR_UNSUPPORTED_EVIDENCE): gap_boxes, parallel needs a face, axis or plane per side
expect_geo(gap_boxes).to_have_spatial_relationships(
    relationships=[dict(kind="parallel", subject="subject", target="target")],
)
```