arris-math 0.9.1

Arris kernel: points, vectors, frames, intervals, exact predicates, root finding and Precision
Documentation

An arris is the sharp edge where two surfaces meet — the thing a B-Rep kernel exists to compute.

Arris is a library, not an application. It builds solids, cuts and fuses them, blends their edges, measures, tessellates and exports them, from Rust or from Python. Four things set it apart:

  • Every operation says what it did. It returns the new body and a provenance record — the faces, edges and vertices it generated, modified and deleted — so a script can pick "the edge the extrude made" instead of an index that shifts when the model changes.
  • It refuses rather than guesses. Geometry it cannot do is a typed error naming the entities involved, never a panic and never an invalid shape; every result passes the invariant checker. Every vertex, edge and face carries its own tolerance instead of one global epsilon.
  • It is deterministic. Same input, same output, same entity ids, on every platform: a body built for wasm32 is byte-identical to the native one.
  • It is pure Rust. No unsafe, no C++ underneath, one crate to depend on, and a Python wheel built from the same code.

Pre-1.0: the API breaks between minor versions. CHANGELOG.md lists every change, and every break with its fix.

Install

Python — a prebuilt wheel for Linux, macOS and Windows, no Rust toolchain needed:

pip install arris

Rust — one crate, arris, re-exports the whole kernel; the API is on docs.rs:

cargo add arris

Example: a bracket in Python

# A motor-mount bracket: extruded L profile, fused boss, bore and bolt pattern cut
# in one call, fillets on the bend and hole rims, and a chamfer on the boss.
# Full script with per-operation face colouring: crates/arris-py/examples/bracket.py

import arris
from arris.build import Solid, Workplane

# 1. Extrude the L plate, then fuse a boss on the upright leg
plate = Workplane.XZ().polygon([(0, 0), (60, 0), (60, 8), (8, 8), (8, 50), (0, 50)]).extrude(-40)
boss = Solid.cylinder((8, 20, 32), (1, 0, 0), 14, 12)
boss = boss.tag("boss", boss.faces())  # a tag follows its faces through every later operation
part = plate.fuse(boss)

# 2. Cut bore, bolt holes and counterbores in one multi-tool call
bore = Solid.cylinder((-1, 20, 32), (1, 0, 0), 8, 23)
holes = [Solid.cylinder((40, y, -1), (0, 0, 1), 3.5, 10) for y in (8, 32)]
wells = [Solid.cylinder((40, y, 5), (0, 0, 1), 6, 4) for y in (8, 32)]
part = part.cut(bore, *holes, *wells)

# 3. Fillets and chamfer: edges selected by provenance, never by index
bend = part.edges().from_role(arris.Role("extrude", "Rise", 0, 3)).one()
part = part.fillet([bend], 4)
skin = part.faces().kind("plane").normal((0, 0, 1)) - part.faces().created_by(wells[0]) - part.faces().created_by(wells[1])
walls = (part.faces().created_by(wells[0]) | part.faces().created_by(wells[1])).kind("cylinder")
rims = walls.edges() & skin.edges()
part = part.fillet(rims, 0.8)
crown = part.faces().tagged("boss")
lip = (crown.kind("plane").edges() & crown.kind("cylinder").edges()).one()
part = part.chamfer([lip], 1.5)

The binding beneath

arris.build is pure Python over the binding, arris.Model, which stays available for anything the layer does not wrap:

# A motor-mount bracket: extruded L profile, fused boss, bore and bolt pattern cut
# in one call, fillets on the bend and hole rims, and a chamfer on the boss.
# Every face is coloured by the operation that made it, followed through provenance.
# The same part on the raw binding; the layer above it is the first snippet.

import arris

model = arris.Model()

# 1. Extrude the L plate
profile = arris.Profile(
    arris.Loop.polygon([(0, 0), (60, 0), (60, 8), (8, 8), (8, 50), (0, 50)]),
    origin=(0, 0, 0), normal=(0, -1, 0), x_axis=(1, 0, 0),
)
plate, plate_rec = model.extrude(profile, (0, 1, 0), 40)

# 2. Fuse a boss on the upright leg
boss, boss_rec = model.primitive_cylinder((8, 20, 32), (1, 0, 0), 14, 12)
body, fuse_rec = model.fuse(plate, boss)

# 3. Cut bore, bolt holes, and counterbores in one multi-tool call
wells = [model.primitive_cylinder((40, y, 5), (0, 0, 1), 6, 4) for y in (8, 32)]
tools = [
    model.primitive_cylinder((-1, 20, 32), (1, 0, 0), 8, 23)[0],
    *(model.primitive_cylinder((40, y, -1), (0, 0, 1), 3.5, 10)[0] for y in (8, 32)),
    *(w for w, _ in wells),
]
body, cut_rec = model.cut_many(body, tools)
history = plate_rec.then(fuse_rec).then(cut_rec)

# 4. Fillets and chamfer: edges selected by provenance
[bend] = [e for e in history.generated_from(arris.Role("extrude", "Rise", 0, 3)) if isinstance(e, arris.Edge)]
body, _ = model.fillet(body, [bend], 4)

rims = [
    e for _, w_rec in wells
    for f in w_rec.generated_from(arris.Role("cylinder", "Wall"))
    for e in cut_rec.generated_from(f)
    if isinstance(e, arris.Edge)
]
body, _ = model.fillet(body, rims, 0.8)

[lip] = [e for e in boss_rec.generated_from(arris.Role("cylinder", "TopRim")) if isinstance(e, arris.Edge)]
body, _ = model.chamfer(body, [lip], 1.5)

What it does

Area What you get
Modelling Primitives, rigid transforms and mirroring; extrude (in any direction) and revolve of profiles of lines, arcs and elliptic arcs; offset faces, shell to a wall, split by a plane; a profile as a one-face sheet body
Sweep and loft Sweep a profile along lines, arcs, helix arcs and B-spline curves, with rotation-minimising or fixed-binormal frames; loft through two or more profiles, ruled or smooth
Booleans Cut, fuse and common, one tool or many in one call, in any pose, over planes, cylinders, elliptic cylinders, cones, spheres, tori and NURBS faces — free-form against analytic and free-form against free-form; results with cavities, split pieces and disjoint shells
Fillet and chamfer Constant radius along chains of edges, with mitres, corners, steps and hole rims; on free-form faces and between two of them, the blend a NURBS surface within your tolerance of the exact one
Queries Ray hits, minimum distance, interference with the shared volume, clash screening of an assembly, the planar section of a body; volume, area, centroid and inertia; face frames and plane projection. None of them changes the body
Meshing Tessellation to a chord tolerance, per face or whole, faces meshed at different times welded together; NumPy arrays from Python
STEP Every solid or surface model in a file read as a checked body or a typed refusal naming why; small gaps closed and reversed faces turned back, each repair named on the body; assemblies; AP214 export in the unit you choose
Other formats STL and OBJ export; a deterministic native format in JSON and bytes, which every later release reads

Not yet

Each of these is a typed refusal today, never a wrong answer:

  • variable-radius fillets and chamfers;
  • offset and shell of elliptic-cylinder and NURBS faces;
  • a NURBS face lying flush against another face over an area;
  • sewing the faces of a STEP file that share no edge;
  • non-manifold bodies.

Arris is narrower than Open CASCADE and is not aiming for feature parity. docs/COMPARISON.md sets out what each does today and where they differ, and its capability matrix, held cell for cell by a test, gives what each operation does and refuses for each face kind.

How it is tested

  • An oracle for every fixture. Each fixture in the test corpus carries values computed by Open CASCADE — volume, area, centroid, counts, point classifications — that Arris must match within the fixture's tolerance. Open CASCADE runs as the oracle through Python and is never a build or runtime dependency (PROVENANCE.md).
  • A checker after every operation. Every result passes the invariant checker, which runs after every operation in debug builds.
  • Properties over random operands. Volume additivity, cut-then-fuse, commutativity, STEP round trips and more, over random operands in random poses from a fixed seed; nightly at five times CI's case count.
  • Real parts. A corpus of real STEP parts is read, checked, cut and blended, and a differential runs random recipes through Arris and Open CASCADE side by side.

Workspace

crates/arris is the facade a consumer depends on. It re-exports the layered crates beneath it, and a crate never depends on one above it (tools/check-layers.sh holds that in CI and in the pre-commit hook):

Crate Holds
arris-math Points, frames, intervals, tolerances, the deterministic transcendentals
arris-geom Curves and surfaces, NURBS, intersections, sections and fits
arris-topo The B-Rep in an arena: bodies, shells, faces, edges, vertices, provenance
arris-check The invariant checker
arris-ops Primitives, booleans, blends, sweeps, lofts, queries
arris-mesh Tessellation
arris-io STEP, STL, OBJ and the native format
arris-py The Python binding, published to PyPI

arris-debug holds the developer tools — the rasteriser, the fixtures, the property-test strategies — and is not published. The layout and its reasons are in docs/ARCHITECTURE.md.

Prior work

Arris stands on four open projects, and it reads them, not copies them. Arris contains no code, comments or identifiers from any of them, and no line-by-line translation. Each one was read for its algorithms and for the mistakes it made, and the approach was reimplemented for Arris's own representation. PROVENANCE.md sets out the rule.

  • Open CASCADE (LGPL 2.1, with an exception). The mature C++ kernel, and the test oracle. Arris runs it through Python to produce expected values; no crate links it, at build or run time. See docs/COMPARISON.md.
  • truck. The original pure-Rust NURBS B-Rep. Arris learns from its NURBS evaluation, its tessellation and its marching intersections, and from why they fail at seams.
  • monstertruck. An active fork of truck, with fillets, healing and more STEP. Arris learns from its fillet and healing work.
  • Fornjot. A validation-first B-Rep kernel, which never reached booleans. Arris learns from its layered validation and its per-object checks.

Licence

MIT or Apache-2.0, at your option. See LICENSE-MIT and LICENSE-APACHE.