Relationships and interference
Component interference and spatial relationships between selected parts, faces and bodies.
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 Geo in TypeScript and raises it in Python. Refusal codes add to the common refusals.
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',
});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",
)to Have No Component Interference
Asserts that no two components of an assembly share material beyond a declared allowance.
Signature
expectGeo(part).toHaveNoComponentInterference(expected?: GeoSpecComponentInterferenceExpectation): GeoSpecAssertionexpect_geo(part).to_have_no_component_interference(tolerance=..., pairs=..., allowances=...) -> GeoSpecAssertionReportFields
| 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 'intentional.
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). Amesh-buffer-v1mesh 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
maxVolumeallowance 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_: fewer than two components.COMPONENT_ PARTITION_ INCONCLUSIVE GEOSPEC_: aSELECTOR_ UNMATCHED pairsentry matches no pair of distinct components.GEOSPEC_: a STEP pair is not shown apart and has no allowance, or itsUNSUPPORTED_ EVIDENCE maxVolumeallowance is not decided.GEOSPEC_: a mesh component is not a closed, consistently oriented shell.INVALID_ EVIDENCE GEOSPEC_: aUNSUPPORTED_ SELECTOR RegExpwith thevflag, outside the engine's regex profile.GEOSPEC_: 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 thanANALYSIS_ LIMIT maxMeshBytesallows.
A failed claim carries GEOSPEC_.
Examples
// 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' }] });# 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"}])to Have Spatial Relationships
Asserts declared relationships between selected parts, faces or bodies.
Signature
expectGeo(part).toHaveSpatialRelationships(expected: GeoSpecSpatialRelationshipsExpectation): GeoSpecAssertionexpect_geo(part).to_have_spatial_relationships(relationships=[...]) -> GeoSpecAssertionReportFields
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 |
angular | 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_); tsc reports an excess property on the forms that run, and as Geo compiles them. Strings, RegExp, body and face selectors compile and run as written.
Admitted geometry
- STEP, by kind:
clearance,contact,insertionandinterferencetake two complete box-shaped occurrences, andinsertiona Cartesianaxis;coaxial,concentric,coplanar,parallel,perpendicularandangletake one analytic face, axis or plane per endpoint;containmenttakes closed solids. Clearance must lie in[min - tolerance, max + tolerance], shared volume in[minVolume, maxVolume]. - glTF/GLB bodies (
{ kind: 'body', of: 'cubeA#0' }) admitcontact,clearance,containmentandinterferenceover closed shells; the other kinds refuse on a mesh.
Refusal codes
GEOSPEC_,SELECTOR_ UNMATCHED GEOSPEC_: a catalog selector orSELECTOR_ AMBIGUOUS RegExpresolves to no entity, or to more than its cardinality allows.GEOSPEC_: an endpoint outside the kind's shapes (a whole occurrence forSELECTOR_ UNSUPPORTED_ EVIDENCE 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_: an insertion outside the route above, such as boxes withoutUNSUPPORTED_ EVIDENCE axis, or geometry outside a proof's domain, named indetails..applicability. condition GEOSPEC_: a mesh body that is not a closed, consistently oriented shell.INVALID_ EVIDENCE GEOSPEC_: aUNSUPPORTED_ SELECTOR RegExpwith thevflag.GEOSPEC_: 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.ANALYSIS_ LIMIT detailscarrylimitandrequired, and no host limit raises them.
A failed claim carries GEOSPEC_.
Examples
// 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' }],
});# 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")],
)