# Compare parts

Select two parts of an assembly by name, claim their clearance or non-interference, and query the exact distance between them.

Canonical page: https://www.geospec.xyz/docs/guides/compare-parts

Claim how two parts of an assembly sit against each other. The parts are the occurrences of a STEP file or the named nodes of a GLB; the engine answers each pair exactly or refuses it, and a selector, an exact occurrence name or a regular expression, must resolve to one entity. The bundled demo models are single parts, so the examples run on fixtures from the engine's repository, copied next to the script.

### 1. Name the parts and claim a clearance [#1-name-the-parts-and-claim-a-clearance]

`packages/geospec-engine/native/occt/rust/tests/fixtures/box-clearance/two-mm.step` holds two 10 mm cubes named `subject` and `target`, 2 mm apart along x.

**TypeScript**

```ts title="check.ts"
import { readFile } from 'node:fs/promises';
import { expectGeo } from 'geospec';
import { GeoSpecAssertionError } from 'geospec/assertion-client';
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 boxes = await loadModel({ source: 'two-mm.step', format: 'step' });

const clearance = (subject: string | RegExp, target: string, min = 2) => {
  try {
    expectGeo(boxes).toHaveSpatialRelationships({ relationships: [{ kind: 'clearance', subject, target, min }] });
    console.log('passed');
  } catch (error) {
    if (!(error instanceof GeoSpecAssertionError)) throw error;
    console.log(error.report.status, (error.diagnostics[0] as { code: string }).code);
  }
};

clearance('subject', 'target');
clearance('subject', 'target', 3);
clearance(/^sub/, 'target');
clearance(/t/, 'target');
clearance('subject', 'nut');

await loadModel.dispose();
engine.close();
```

A failed or refused chain throws `GeoSpecAssertionError`, which fails the test it is in under your own test runner; the script catches it to print every verdict:

```console
$ node check.ts
passed
failed GEOSPEC_SPATIAL_RELATIONSHIP_MISMATCH
passed
refused GEOSPEC_SELECTOR_AMBIGUOUS
refused GEOSPEC_SELECTOR_UNSUPPORTED_EVIDENCE
```

**Python**

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

with GeoSpecEngine(model_root="."):
    boxes = load_model(file="two-mm.step", format="step")

    def clearance(subject, target, minimum=2):
        relationship = {"kind": "clearance", "subject": subject, "target": target, "min": minimum}
        report = evaluate_geo(boxes, "toHaveSpatialRelationships", {"relationships": [relationship]})
        print(report.result["status"], *(diagnostic["code"] for diagnostic in report.diagnostics))

    clearance("subject", "target")
    clearance("subject", "target", minimum=3)
    clearance(GeoSpecRegex("^sub"), "target")
    clearance(GeoSpecRegex("t"), "target")
    clearance("subject", "nut")
```

`evaluate_geo` returns the report whatever the outcome; `expect_geo` raises at the first failed or refused claim, failing its test under pytest ([Test parts in CI](https://www.geospec.xyz/docs/guides/test-parts-in-ci.mdx)).

```console
$ python check.py
passed
failed GEOSPEC_SPATIAL_RELATIONSHIP_MISMATCH
passed
refused GEOSPEC_SELECTOR_AMBIGUOUS
refused GEOSPEC_SELECTOR_UNSUPPORTED_EVIDENCE
```

### 2. Read a clearance [#2-read-a-clearance]

The first claim passes with `[{ "distance": 2 }]` as its measured evidence. The second asks for 3 mm and fails; its witnesses hold the closest points, (10, 0, 0) and (12, 0, 0), as exact rationals. `^sub` matches one occurrence and passes; `t` matches both cubes, so the claim is refused as ambiguous; `nut` names no occurrence; the claim is refused because the file carries no authored interface names to check it against.

### 3. Claim no interference [#3-claim-no-interference]

On STEP, `toHaveNoComponentInterference` refuses with `GEOSPEC_UNSUPPORTED_EVIDENCE` unless a pair carries an allowance ([relationships reference](https://www.geospec.xyz/docs/reference/relationships.mdx)). On a GLB it computes each pair's shared volume exactly, with components labelled `node#primitive`. `packages/geospec-engine/bench/fixtures/performance-lab/generated/assembly.glb` holds nodes `cubeA` and `cubeB`, 20 mm apart, and loads with `sourceUnit: 'mm'`:

* `toHaveNoComponentInterference()` passes; the pair reads `intersectionNumerator: "0"` and `withinAllowance: true` under the measured evidence.
* `toHaveNoComponentInterference({ pairs: [{ left: 'cubeA#0', right: 'pin#0' }] })` is refused with `GEOSPEC_SELECTOR_UNMATCHED`: no component is named `pin#0`.
* A clearance of `min: 20` between `{ kind: 'body', of: 'cubeA#0' }` and `{ kind: 'body', of: 'cubeB#0' }` passes with `distanceSquaredNumerator: "400"`, 20 mm as an exact square; a plain `'cubeA'` resolves STEP occurrences only.

### 4. Query the distance [#4-query-the-distance]

**TypeScript**

`minimumDistance` reports the exact distance between two occurrences of an AP242 STEP assembly, with a closest point on each, through `client.query` from [Query geometry](https://www.geospec.xyz/docs/guides/query-geometry.mdx) with `payload: { pair: [{ occurrencePath: 'subject' }, { occurrencePath: 'target' }] }`. Its result is typed rather than a report: `complete` with a `fact`, on the two cubes `distance: 2`, `points: [[10, 0, 0], [12, 0, 0]]` and `assurance: 'exact-brep'`; `refused` with a `code` and `message`, `unsupported-evidence` on a mesh; or `interrupted`.

**Python**

`query_geo(boxes, "minimumDistance", {"pair": [{"occurrencePath": "subject"}, {"occurrencePath": "target"}]})` reports the exact distance between two AP242 occurrences: on the two cubes, `result["evidence"]["fact"]` has `distance` 2. A mesh is `refused`.

### 5. Read a partition refusal [#5-read-a-partition-refusal]

```ts
expectGeo(part)
  .toHaveNoComponentInterference();
```

The plate and the gear are single parts, so the claim is refused with `GEOSPEC_COMPONENT_PARTITION_INCONCLUSIVE`: there is no second component to compare, and the engine says so rather than passing an empty check. Its suggestion names the fix: one glTF node per part, or a STEP assembly with named occurrences, as in steps 1 and 3.

## Variations [#variations]

**Contact and alignment.** `contact` asks two parts to touch within `tolerance`; the 2 mm gap fails it. Alignment kinds such as `parallel` need a face, axis or plane selector on each side; a whole occurrence is refused with `GEOSPEC_SELECTOR_UNSUPPORTED_EVIDENCE`.

**Pairs you mean to touch.** An `allowances` entry with `left`, `right`, `reason` and `kind: 'intentionalInterference'` waives a pair; with one for `subject` and `target`, the STEP claim of step 3 passes.

**Limits the file carries.** `toSatisfyParallelPlaneDistance()` takes no argument: the STEP file declares the two faces and the band between them. The two-cube file declares nothing, so the claim is refused with `GEOSPEC_CERTIFICATE_UNVERIFIED`; the [contracts reference](https://www.geospec.xyz/docs/reference/contracts.mdx#tosatisfyparallelplanedistance) describes the declaration.