Write a spec from a drawing
Map each callout on a part drawing to a matcher, take the tolerance from the tolerance block and run the spec against the export.
A drawing says what a part must be. A spec says the same as claims the engine can decide: one callout, one matcher, one expectation. The claims come from the drawing; the measurements come from the model.
Map the callouts
| Callout | Claim |
|---|---|
| Overall 40 × 40 × 20 | toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } }) |
| Ø10 on centre, along z | toHaveCircularHole({ diameter: 10, axis: 'z', center: { x: 0, y: 0 } }) |
| 6 × Ø5 on a Ø45 bolt circle | to |
| 1 × 45° chamfer | to |
| Fillet radius 2 | to |
| Wall 2 min | to |
| Volume | toHaveVolume({ value: 30429.2 }) |
| Mass, aluminium 2.7 g/cm³ | toHaveMass({ value: 82.16, density: 0.0027 }) |
Python spells each matcher in snake case with the same keys as keyword arguments: to_. Every feature row needs a STEP export; three rows need care:
- Holes.
toHaveCircularHoledecides diameter, axis, centre andthrough; it has no depth field, so a depth callout stays a requirement the spec reports as unsupported. - Wall thickness. The engine measures the exact minimum of a complete box or a right cylinder only (admitted geometry); a bored plate is neither, so the claim refuses with
GEOSPEC_. Keep the callout and report it as unsupported.UNSUPPORTED_ EVIDENCE - Mass.
densityin g/mm³ gives grams. Without it the claim refuses on every subject.
Take the tolerance from the drawing
A tolerance block of ±0.1 becomes tolerance: 0.1 on the claims it covers; a claim without one is held to 0.02 mm (Set tolerances and budgets). A limit such as "2 min" is a strict comparison, greaterThanOrEqual, with no tolerance. The tolerance never moves to make a claim pass: a failure at the drawing's tolerance is a model that misses the drawing.
The plate
The drawing in words: a 40 × 40 × 20 mm aluminium plate, origin at the centre of its bottom face, Ø10 bore on centre along z. Its volume is 40 × 40 × 20 less π × 5² × 20, or 30429.2 mm³; at 0.0027 g/mm³ that is 82.16 g, centre of mass [0, 0, 10].
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) .toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } });expectGeo(part) .toHaveCircularHole({ diameter: 10, axis: 'z', center: { x: 0, y: 0 } });expectGeo(part) .toHaveVolume({ value: 30429.2 });expectGeo(part) .toHaveMass({ value: 82.16, density: 0.0027 });expectGeo(part) .toHaveCenterOfMass({ point: [0, 0, 10] });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.
All five pass. Set x to 41, or diameter to 12, and that claim fails with the measured value in its diagnostic; the drawing's number then comes from the model.
As a file, give each callout its own test: a failed or refused claim ends the test it is in, so one test per callout keeps every verdict visible. GeoSpec for agents runs a spec from a shell.
Variations
A mesh export. On a GLB of the part the size, volume, mass and centre rows still answer; the bore row refuses with GEOSPEC_. Read a refusal says what to change.
A range on the drawing. "30400 to 30500 mm³" is toHaveVolume({ value: { greaterThan: 30400, lessThan: 30500 } }), in Python to_; the comparisons are strict and carry no tolerance.
A part with no drawing. Query analyzeBrep to list the holes, patterns, chamfers and fillets the model carries, then write claims from the requirement, not the inventory. A claim copied from the measurement passes on every model, including a wrong one.