Compare parts
Select two parts of an assembly by name, claim their clearance or non-interference, and query the exact distance between them.
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/ holds two 10 mm cubes named subject and target, 2 mm apart along x.
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 Geo, 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_EVIDENCEfrom 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_EVIDENCE2. 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, to refuses with GEOSPEC_ 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/ holds nodes cubeA and cubeB, 20 mm apart, and loads with sourceUnit: 'mm':
topasses; the pair readsHave No Component Interference( ) intersectionandNumerator: "0" withinAllowance: trueunder the measured evidence.tois refused withHave No Component Interference( { pairs: [{ left: 'cubeA#0', right: 'pin#0' }] }) GEOSPEC_: no component is namedSELECTOR_ UNMATCHED pin#0.- A clearance of
min: 20between{ kind: 'body', of: 'cubeA#0' }and{ kind: 'body', of: 'cubeB#0' }passes withdistance, 20 mm as an exact square; a plainSquared Numerator: "400" '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[ 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
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();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_: 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_.
Pairs you mean to touch. An allowances entry with left, right, reason and kind: 'intentional waives a pair; with one for subject and target, the STEP claim of step 3 passes.
Limits the file carries. to 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_; the contracts reference describes the declaration.