# Measurements

Bounding box, surface area, volume, mass and centre of mass.

Canonical page: https://www.geospec.xyz/docs/reference/measurement

These assertions measure the whole subject. A STEP subject is measured from its BRep; a mesh, from its triangles. The engine picks the evidence from the subject, and the report's evidence names the source it used (`brep` or `mesh`).

`plate` and `box` are the site's demo models; `tessellated` is a repository fixture, a STEP box whose faces carry no surface. Python matchers are snake\_case; their keywords keep the camelCase field names (`toleranceMm=`). A failed or refused call throws `GeoSpecAssertionError` in TypeScript and raises it in Python. Refusal codes add to the [common refusals](https://www.geospec.xyz/docs/reference/overview.mdx#common-refusals).

**TypeScript**

```ts title="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 plate = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });
const box = await loadModel({ source: 'box-10x20x30.step', format: 'step' });
const tessellated = await loadModel({
  source: 'packages/geospec-engine/native/occt/rust/tests/fixtures/read-profile/box-tessellated-only.step',
  format: 'step',
});
```

**Python**

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

with GeoSpecEngine(model_root="."):
    plate = load_model(file="plate-with-through-bore.step", format="step")
    box = load_model(file="box-10x20x30.step", format="step")
    tessellated = load_model(
        file="packages/geospec-engine/native/occt/rust/tests/fixtures/read-profile/box-tessellated-only.step",
        format="step",
    )
```

## toHaveBoundingBox [#tohaveboundingbox]

Asserts the axis-aligned bounds of the subject.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveBoundingBox(first: Vec3 | GeoSpecBoundingBoxExpectation, second?: Vec3): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_bounding_box(min=..., max=..., size=..., center=..., tolerance=...) -> GeoSpecAssertionReport
expect_geo(part).to_have_bounding_box(first, second, /) -> GeoSpecAssertionReport
```

Two points are `min` and `max`.

**Fields**

| Field                          | Type                                                                | Default     | Meaning                                                              |
| ------------------------------ | ------------------------------------------------------------------- | ----------- | -------------------------------------------------------------------- |
| `min`, `max`, `size`, `center` | `[x, y, z]`, or `{ x?, y?, z? }` of numbers or numeric expectations | not checked | Corners, extent and centre; each declared axis is checked on its own |
| `tolerance`                    | number, 0 or more                                                   | `0.02`      | Allowed error per axis                                               |

**Admitted geometry**

* STEP: the BRep bounds.
* Mesh and glTF/GLB: the bounds of the triangle vertices. An open mesh still has bounds, so the claim passes on one triangle.

**Refusal codes**

The common refusals only. A failed claim carries `GEOSPEC_BOUNDING_BOX_MISMATCH`, naming each axis that missed. A negated claim that matches fails with `GEOSPEC_NEGATED_MATCH`, or with `GEOSPEC_NEGATED_BOUNDING_BOX_MATCH` on a `mesh-buffer-v1` subject.

**Examples**

**TypeScript**

```ts
// passed: plate, 40 × 40 × 20 mm with the bore inside the box
expectGeo(plate).toHaveBoundingBox({ size: { x: 40, y: 40, z: 20 } });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): tessellated, its six faces have no surface
expectGeo(tessellated).toHaveBoundingBox({ size: { x: 10, y: 20, z: 30 } });
```

**Python**

```python
# passed: plate, 40 × 40 × 20 mm with the bore inside the box
expect_geo(plate).to_have_bounding_box(size={"x": 40, "y": 40, "z": 20})
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): tessellated, its six faces have no surface
expect_geo(tessellated).to_have_bounding_box(size={"x": 10, "y": 20, "z": 30})
```

## toHaveSurfaceArea [#tohavesurfacearea]

Asserts the total surface area of the subject.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveSurfaceArea(expected: GeoSpecSurfaceAreaExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_surface_area(value=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field       | Type                     | Default  | Meaning                  |
| ----------- | ------------------------ | -------- | ------------------------ |
| `value`     | numeric expectation, mm² | required | The total area           |
| `tolerance` | number, 0 or more        | `0.02`   | Allowed error on `value` |

**Admitted geometry**

* STEP: the BRep surface area.
* Mesh and glTF/GLB: the sum of the triangle areas. A mesh approximates curved faces, so its area differs from the BRep area of the same part.

**Refusal codes**

The common refusals only. A failed claim carries `GEOSPEC_MEASUREMENT_MISMATCH`, with the measured value and its evidence source.

**Examples**

**TypeScript**

```ts
// passed: box, 2 × (200 + 300 + 600) mm²
expectGeo(box).toHaveSurfaceArea({ value: 2200 });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): tessellated, its six faces have no surface
expectGeo(tessellated).toHaveSurfaceArea({ value: 2200 });
```

**Python**

```python
# passed: box, 2 × (200 + 300 + 600) mm²
expect_geo(box).to_have_surface_area(value=2200)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): tessellated, its six faces have no surface
expect_geo(tessellated).to_have_surface_area(value=2200)
```

## toHaveVolume [#tohavevolume]

Asserts the enclosed volume of the subject.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveVolume(expected: GeoSpecVolumeExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_volume(value=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field       | Type                     | Default  | Meaning                  |
| ----------- | ------------------------ | -------- | ------------------------ |
| `value`     | numeric expectation, mm³ | required | The enclosed volume      |
| `tolerance` | number, 0 or more        | `0.02`   | Allowed error on `value` |

**Admitted geometry**

* STEP: the BRep volume.
* Mesh and glTF/GLB: the absolute signed volume of the triangles. An open mesh still yields a number, so pair the claim with `toBeWatertight`.

**Refusal codes**

The common refusals only. A failed claim carries `GEOSPEC_MEASUREMENT_MISMATCH`.

**Examples**

**TypeScript**

```ts
// passed: plate, 32,000 mm³ less the bore is 30,429.2 to one decimal
expectGeo(plate).toHaveVolume({ value: 30429.2, tolerance: 0.5 });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): tessellated, its six faces enclose no exact volume
expectGeo(tessellated).toHaveVolume({ value: 6000 });
```

**Python**

```python
# passed: plate, 32,000 mm³ less the bore is 30,429.2 to one decimal
expect_geo(plate).to_have_volume(value=30429.2, tolerance=0.5)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): tessellated, its six faces enclose no exact volume
expect_geo(tessellated).to_have_volume(value=6000)
```

## toHaveMass [#tohavemass]

Asserts the mass of the subject, derived as volume × `density`.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveMass(expected: GeoSpecMassExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_mass(value=..., density=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field       | Type                | Default  | Meaning                                                 |
| ----------- | ------------------- | -------- | ------------------------------------------------------- |
| `value`     | numeric expectation | required | In the unit `density` implies: grams per mm³ give grams |
| `density`   | number, 0 or more   | none     | Mass per mm³                                            |
| `tolerance` | number, 0 or more   | `0.02`   | Allowed error on `value`                                |

**Admitted geometry**

* The volume comes from the same evidence as `toHaveVolume`.
* No admitted subject carries a material mass, so a claim without `density` refuses on every subject.

**Refusal codes**

* `GEOSPEC_UNSUPPORTED_EVIDENCE`: the claim declares no `density`.

A failed claim carries `GEOSPEC_MEASUREMENT_MISMATCH`.

**Examples**

**TypeScript**

```ts
// passed: box, 6,000 mm³ of aluminium at 0.0027 g/mm³ is 16.2 g
expectGeo(box).toHaveMass({ value: 16.2, density: 0.0027 });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): box, no density declared
expectGeo(box).toHaveMass({ value: 16.2 });
```

**Python**

```python
# passed: box, 6,000 mm³ of aluminium at 0.0027 g/mm³ is 16.2 g
expect_geo(box).to_have_mass(value=16.2, density=0.0027)
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): box, no density declared
expect_geo(box).to_have_mass(value=16.2)
```

## toHaveCenterOfMass [#tohavecenterofmass]

Asserts the centre of mass of the subject, assuming uniform density.

**Signature**

**TypeScript**

```ts
expectGeo(part).toHaveCenterOfMass(expected: GeoSpecCenterOfMassExpectation): GeoSpecAssertion
```

**Python**

```python
expect_geo(part).to_have_center_of_mass(point=..., tolerance=...) -> GeoSpecAssertionReport
```

**Fields**

| Field       | Type              | Default  | Meaning                                                                   |
| ----------- | ----------------- | -------- | ------------------------------------------------------------------------- |
| `point`     | point of numbers  | required | Each declared axis is compared on its own; `{ x: 0, y: 0 }` leaves z free |
| `tolerance` | number, 0 or more | `0.02`   | Allowed error per axis                                                    |

**Admitted geometry**

* STEP: the BRep centre of mass of a solid with nonzero volume.
* Mesh and glTF/GLB: the centroid of the enclosed volume. An open mesh still yields a point, so pair the claim with `toBeWatertight`.

**Refusal codes**

* `GEOSPEC_UNSUPPORTED_EVIDENCE`: the subject has no centre of mass, such as a STEP shape with zero volume.

A failed claim carries `GEOSPEC_MEASUREMENT_MISMATCH`.

**Examples**

**TypeScript**

```ts
// passed: plate, centred on the z axis with z from 0 to 20
expectGeo(plate).toHaveCenterOfMass({ point: [0, 0, 10] });
// refused (GEOSPEC_UNSUPPORTED_EVIDENCE): tessellated, its six faces enclose no exact volume
expectGeo(tessellated).toHaveCenterOfMass({ point: [5, 10, 15] });
```

**Python**

```python
# passed: plate, centred on the z axis with z from 0 to 20
expect_geo(plate).to_have_center_of_mass(point=[0, 0, 10])
# refused (GEOSPEC_UNSUPPORTED_EVIDENCE): tessellated, its six faces enclose no exact volume
expect_geo(tessellated).to_have_center_of_mass(point=[5, 10, 15])
```