GeoSpec
engine 0.4.0

Compare parts

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

Open Markdown

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

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.

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:

$ node check.ts
passed
failed GEOSPEC_SPATIAL_RELATIONSHIP_MISMATCH
passed
refused GEOSPEC_SELECTOR_AMBIGUOUS
refused GEOSPEC_SELECTOR_UNSUPPORTED_EVIDENCE
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).

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

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

On STEP, toHaveNoComponentInterference refuses with GEOSPEC_UNSUPPORTED_EVIDENCE unless a pair carries an allowance (relationships reference). 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

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

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

const part = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });

Tab indents. Press Escape, then Tab, to leave the editor.

The spec as a file, with the model loaded over the native addon
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 part = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });expectGeo(part)  .toHaveNoComponentInterference();await loadModel.dispose();engine.close();

The engine loads in this tab when you run the spec.

Model
Select a claim to see what it checks.

plate-with-through-bore.step · 19 KB · copied from an engine test fixture

Run the spec to see what the engine answers for each claim. Then change an expected value and run it again.

 

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

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 describes the declaration.

On this page