@exornea/mensura
v0.3.1
Published
Spatial math and geometry kernel for TypeScript game and editor runtimes.
Maintainers
Readme
Mensura
Mensura is a small Geukbit-grade spatial math and geometry kernel for TypeScript game and editor runtimes.
It is intentionally lower-level than Geukbit: Mensura owns the reusable geometry math that a Geukbit viewport, editor, or runtime can dogfood, but it should not know about scenes, entities, components, assets, inspectors, materials, or renderers.
Scope
Mensura owns reusable math primitives:
- floating-point comparison and ULP helpers
- conversion loss functions for
number -> f32 - vectors, quaternions, and matrices
- rays, planes, AABBs, spheres, and frustums
- intersection and overlap tests
- closest-point, bounds, and signed-distance measurements
- collision narrowphase (SAT, GJK, EPA, MPR, CCD) and acceleration structures
- WGSL layout metadata and checked DataView projection
- grid/world coordinate conversion
- transform compose/decompose helpers
- WebGPU viewport project/unproject helpers
Coordinate And Memory Policy
Mensura uses a right-handed world coordinate system:
+Xis right+Yis up-Zis forward
Matrices use column-major storage and column-vector multiplication:
- points are transformed as
p' = M * p - transform composition is
M = T * R * S - model-view-projection is
MVP = P * V * M
Projection helpers target WebGPU-style clip depth by default:
- NDC X:
-1..1 - NDC Y:
-1..1 - NDC Z:
0..1
The public API uses immutable number objects by default. Hot-path APIs use
Into suffixes and write into caller-owned outputs. Packed GPU bridges use
Float32Array. Binary projection bridges use DataView.
Ray intersections only return hits with distance >= 0. AABB point containment
is inclusive. Grid cell ownership uses Math.floor.
See Coordinate And Matrix Policy.
Module Layout
@exornea/mensura facade for the stable kernel surface
@exornea/mensura/core float, vector, and matrix math
@exornea/mensura/geometry shape primitives: rays, planes, bounds, spheres
@exornea/mensura/query ray hits, overlap tests, frustum culling
@exornea/mensura/collision SAT, GJK, EPA, MPR, and CCD narrowphase
@exornea/mensura/accel BVH and broadphase acceleration structures
@exornea/mensura/world collision world orchestration
@exornea/mensura/layout WGSL-compatible byte layout metadata
@exornea/mensura/data checked DataView projection records
@exornea/mensura/measure closest points, bounds, areas, barycentric data
@exornea/mensura/validation Result-first checks and reproducible seed helpers
@exornea/mensura/batch object-array batch kernels for hot loops
@exornea/mensura/physics compatibility facade for accel/collision/world
@exornea/mensura/gpu WebGPU projection and packed Float32Array bridges
@exornea/mensura/unsafe unchecked binary and typed-array projection helpers
@exornea/mensura/wasm WebAssembly feature probes for optional kernelsFor 0.3.x, core, geometry, query, measure, validation, gpu,
layout, data, and batch are the stable release surface. collision,
accel, and world are experimental dogfood layers. physics is a
compatibility facade. wasm is an experimental feature-probe layer. unsafe
is explicitly unsafe and opt-in.
See API Stability for the release contract. See API Guide for layer-by-layer usage notes. See Migration To 0.3.0 for the collision support-map API change and package-consumption notes. See Collision Layer for SAT/GJK/EPA/MPR/CCD boundary semantics. See Math Theory for the formulas behind float32 loss, empty-domain sentinels, RNG algorithms, sampling distributions, geometric samplers, and bias diagnostics.
The root facade exports the stable primary layers. physics remains as a legacy
compatibility facade for older imports, but new code should import query, collision,
accel, and world by responsibility. measure owns derived primitive
measurements and projections such as AABB closest points, capsule bounds,
triangle normals, areas, barycentric coordinates, and triangle closest points.
It also exposes *Checked variants for boundary callers that want invalid
measure domains surfaced as Result errors.
validation owns Result-first precondition checks for finite values,
non-empty bounds, non-degenerate triangles, stable float32 conversion loss,
deterministic seed/RNG/distribution helpers for reproducible stress or benchmark
inputs, and observation suitability gates before measurement/comparison.
layout describes byte-level records; data is the checked Result-first
bridge from semantic values into those records. batch keeps the inspectable
object policy while amortizing call overhead across many values.
Rule of thumb: if the answer is yes/no, import from
query. If the answer is a value (point, vector, distance, area), import frommeasure. Shape values themselves (constructors, mutable variants, copies) stay ingeometry. If the question is whether a value is safe enough to measure or serialize, usevalidation. If the question is whether a sample set is suitable enough to compare or analyze, usevalidation's observation gate before computing the measurement.
unsafe is intentionally not re-exported by the root facade. Import it by name
when a caller owns the buffer layout, bounds checks, and aliasing contract.
Collision Contract
@exornea/mensura/collision is still an experimental dogfood layer, but it is
not a collection of placeholders. Public collision entry points must either
implement their algorithm, return a documented Result failure, or stay out of
the public package.
testObbObbSatis inclusive for touching OBB boundaries.gjkis the caller-owned-output support-map path for GJK. Exact touching reportsintersect: falsebecause the support advance is not strictly positive.mprIntersectruns Minkowski Portal Refinement directly for binary convex intersection over{ center, supportInto }shapes. It returns{ intersect, portalDirection, portalRefined, iterations }and uses the same strict boundary policy as GJK.epais the penetration-depth recovery path after a real GJK 4-simplex.- CCD helpers report first future contact events, with documented initial overlap behavior per shape family.
Failure Model
Operations that can fail at the boundary return Result<T> data rather than
throwing. The shape is:
type Result<T> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: MensuraError };Common error codes: VALIDATION_INVALID_FORMAT (bad perspective arguments),
TRANSFORM_SINGULAR (det = 0 inversion), TRANSFORM_DEGENERATE_BASIS
(lookAt with eye = center, quatFromUnitVectors anti-parallel without a
stable axis), GJK_MAX_ITERATIONS, EPA_MAX_ITERATIONS, and
MPR_MAX_ITERATIONS for collision iteration budgets.
MensuraError.code and MensuraError.stage are typed literal unions, not raw
strings. Use result.ok, isOk, isErr, matchResult, or unwrapOr when
failures should remain data. Use unwrap(result) only at fail-fast boundaries
such as tests, examples, CLI, or CI.
Policy
Numerical thresholds and validation bounds are collected in DEFAULT_POLICY
so that decisions are visible as data, not magic constants. Individual
constants (QUAT_SLERP_LINEAR_THRESHOLD, QUAT_PARALLEL_EPSILON,
PERSPECTIVE_MIN_FOV_Y_RADIANS, PERSPECTIVE_MAX_FOV_Y_RADIANS,
DEFAULT_FLOAT_TOLERANCE) are also exported for direct reference.
First API
import {
distanceSq3,
lossF32,
nearlyEqualUlpsF32,
ulpDiffF32,
unwrap,
vec3
} from "@exornea/mensura/core";
import {
aabb,
frustumFromMatrixWebGpu,
ray,
} from "@exornea/mensura/geometry";
import { frustumIntersectsAabb, rayAabbHitDistance } from "@exornea/mensura/query";
import { mat4PerspectiveWebGpuRh } from "@exornea/mensura/gpu";
const a = vec3(0, 0, 0);
const b = vec3(1, 2, 3);
const projection = unwrap(mat4PerspectiveWebGpuRh(Math.PI / 2, 16 / 9, 0.1, 100));
const frustum = frustumFromMatrixWebGpu(projection);
const bounds = aabb(vec3(-1, -1, -5), vec3(1, 1, -3));
const pickRay = ray(vec3(0, 0, 0), vec3(0, 0, -1));
console.log(distanceSq3(a, b));
console.log(ulpDiffF32(1, Math.fround(1 + Number.EPSILON)));
console.log(nearlyEqualUlpsF32(1, Math.fround(1), 0));
console.log(lossF32(1 + Number.EPSILON));
console.log(frustumIntersectsAabb(frustum, bounds));
console.log(rayAabbHitDistance(pickRay, bounds));Worked examples (camera+frustum, TRS compose/decompose, quaternion operations, Result-based error handling, collision source-of-truth flow, and visual ray fixtures) live under examples/.
Run the collision example after a build:
npm run example:collisionVisual Ray Fixtures
Ray hit tests can be inspected without adding browser or graphics dependencies:
npm run visual:rayThis builds the package and runs
examples/visual-ray-fixtures.mjs, writing
dependency-free SVG, 2D HTML, 3D Canvas HTML, and JSON manifest artifacts to
.mensura-visual/. The artifacts are for human review; Vitest keeps a
regression test that the rendered manifest values match the actual ray API
calculations.
The checked-in preview at examples/visual-ray-fixtures-3d-preview.png shows the intended 3D layout without running the generator.
Performance Snapshot
Latest local benchmark snapshot:
Node: v22.17.0
Samples: 7 median after 2 warmups
Date: 2026-05-20Selected release-gated results:
| Case | Mensura | Reference / baseline | Ratio |
|---|---:|---:|---:|
| add3Into | 108.7M ops/s | gl-matrix vec3.add 96.9M ops/s | 1.12x |
| normalize3Into | 93.5M ops/s | gl-matrix vec3.normalize 83.2M ops/s | 1.12x |
| mat4MultiplyInto | 44.1M ops/s | gl-matrix mat4.multiply 37.4M ops/s | 1.18x |
| affinePoint3Into | 98.1M ops/s | gl-matrix vec3.transformMat4 63.2M ops/s | 1.55x |
| add3IntoMany | 280.8M ops/s | gl-matrix loop 174.6M ops/s | 1.61x |
| unsafeVec3AddF32Many | 729.7M ops/s | scalar object loop 190.0M ops/s | 3.84x |
| unsafeVec3DotF32Many | 806.5M ops/s | dot3IntoMany 369.6M ops/s | 2.18x |
| unsafeVec3CrossF32Many | 524.4M ops/s | cross3IntoMany 272.2M ops/s | 1.93x |
npm run benchmark is the authoritative comparison harness used by
benchmark:check. perf-benchmark.ts is only a smaller focused local probe,
not the release gate:
npm run build
node --experimental-strip-types perf-benchmark.tsSee Performance for the full table, interpretation, and API selection guide.
Release Gate
npm run check:releaseThis runs public-source stub checks, build, tests, npm pack --dry-run, and
the benchmark threshold gate. If a release-blocking hot path falls below its
relative performance floor, or if public src/ code still describes itself as
unimplemented placeholder work, the command fails.
The release gate also runs DCO validation, the experimental-module build,
the API surface snapshot, the dist/*.d.ts symbol snapshot, the packaged
bundler-resolution smoke test, and a Vite browser bundle smoke test. Generated
experimental and smoke outputs live in ignored folders.
Contributions
All contribution commits require a DCO sign-off:
git commit -sBefore opening a pull request, run:
npm run dco:check -- --range origin/master..HEADSee CONTRIBUTING.md and DCO.md.
Non-Goals
- no renderer handles
- no scene graph
- no entity/component model
- no asset or material catalog
- no full physics engine, solver, integrator, or rigid-body runtime
- no editor UI state
Mensura should remain a geometry kernel that Geukbit, Zeno benchmarks, and other game/editor packages can share.
