# `Quiddity`
[🔗](https://github.com/d-led/ex.quiddity/blob/main/lib/quiddity.ex#L1)

Manufacturing features of a STEP part, and the geometry under them, from
Elixir.

Two questions, and quiddity answers both from the B-rep alone — no CAD system,
no feature tree, no OpenCascade, and the same file always gives the same
answer:

  * **what is this part?** — `features/1` names its holes, pockets, slots,
    bosses, steps, patterns, fillets, chamfers, grooves, springs and turned
    profiles, with their measured sizes and positions, from
    [quiddity](https://github.com/pzfreo/quiddity);
  * **where is it?** — `ray/4`, `frame/2`, `samples/2` and `face/2` answer in
    millimetres against the exact surfaces, from `haecceity`, the kernel
    quiddity re-exports.

```elixir
{:ok, part} = Quiddity.open("bracket.step")
{:ok, recognition} = Quiddity.features(part)

Recognition.families(recognition)
#=> [{"holes", 4}, {"fillets", 6}, {"pockets", 1}]

{:ok, camera} = Quiddity.Camera.looking_at([0, 0, 120], [0, 0, 0])
{:ok, frame} = Quiddity.frame(part, camera)
Frame.coverage(frame)
#=> 0.62
```

## Why a handle

`open/1` reads the file once and answers a `Quiddity.Part` holding the parsed
B-rep. Everything else takes that handle, so a part is parsed once however many
questions follow. A B-rep is immutable, so one handle serves every process on
the node at once — and, because it is a resource rather than a term, it cannot
be sent to another node, which is the honest boundary: geometry is local.

## The camera is geometry, not a learner

`Quiddity.Camera` and `Quiddity.Frame` are a pinhole camera and the frame it
renders against the exact B-rep. `Camera.cam_to_world/1` and
`Frame.semantic_3d/2` answer the two matrices a sensor model conventionally
takes; what a perception or learning framework makes of them is that
framework's business, in that framework's package. Nothing here interprets
them.

## Limits, said out loud

quiddity reports what it can prove from the geometry. A file that reads without
errors is not a promise every feature was found: `Recognition.unexplained_faces/1`
is where that promise is kept, and `Recognition.unproved/1` names the records
whose evidence it could not verify. A refusal is a record too, so nothing is
silently dropped.

# `capabilities`

```elixir
@spec capabilities() :: {:ok, map()} | {:error, Quiddity.Error.t()}
```

What this build of quiddity can do, as the versioned document it publishes.

Capabilities are the engine's own statement, not this library's: it says which
recognition schema is current, which fingerprint version is in use, and whether
the optional PMI interchange is compiled in.

# `classify`

```elixir
@spec classify(Quiddity.Part.t(), [number()]) ::
  {:ok, map()} | {:error, Quiddity.Error.t()}
```

Whether a point in space is inside the material, outside it, on a face, or
something the classifier will not decide (`"unknown"` — every ray ambiguous,
or a surface it cannot intersect).

# `correspond`

```elixir
@spec correspond(Path.t(), Path.t()) :: {:ok, map()} | {:error, Quiddity.Error.t()}
```

What happened to each feature between two revisions.

Each side is a path to a STEP file or to a recognition JSON file. The answer
lists what was `carried`, `adapted` (with what changed), `orphaned` or
`ambiguous` — more than one candidate, so none is guessed — and which features
are new.

# `evidence`

```elixir
@spec evidence(Quiddity.Part.t(), boolean()) ::
  {:ok, map()} | {:error, Quiddity.Error.t()}
```

The evidence each feature rests on, and every family that was refused.

`framed` asks for part coordinates, which need an inferred frame; when the
frame cannot be inferred the document says it was written in the file's own
coordinates instead. A refused evidence view stays an explicit error in the
document rather than an empty one — an empty view looks like a part with
nothing in it.

# `face`

```elixir
@spec face(Quiddity.Part.t(), non_neg_integer()) ::
  {:ok, Quiddity.Face.t()} | {:error, Quiddity.Error.t()}
```

One face: its surface, area, centroid, normal and neighbours.

# `features`

```elixir
@spec features(Quiddity.Part.t()) ::
  {:ok, Quiddity.Recognition.t()} | {:error, Quiddity.Error.t()}
```

What quiddity recognised in the part.

Every face of the B-rep is accounted for: part of a feature, part of the body
with the reason, refused, or unexplained.

# `fingerprints`

```elixir
@spec fingerprints(Quiddity.Part.t()) :: {:ok, map()} | {:error, Quiddity.Error.t()}
```

Each feature's fingerprint: the revision-independent identity two revisions are
matched through, which is why a part that was moved or re-modelled still
corresponds.

# `frame`

```elixir
@spec frame(Quiddity.Part.t(), Quiddity.Camera.t()) ::
  {:ok, Quiddity.Frame.t()} | {:error, Quiddity.Error.t()}
```

One camera frame: what each pixel of the camera landed on.

Rays are cast against the exact B-rep, so the answer is where the surface *is*,
not a depth buffer's approximation of it.

# `loaded?`

```elixir
@spec loaded?() :: boolean()
```

Whether the Rust engine is built and loaded in this VM.

# `mass`

```elixir
@spec mass(Quiddity.Part.t(), keyword()) ::
  {:ok, map()} | {:error, Quiddity.Error.t()}
```

Volume, area, centroid and — when a density is given — mass of every solid.

`density` is grams per cubic millimetre (steel is 0.00785). `nil` answers no
mass: a B-rep does not say what the part is made of, and guessing a material
would make a number that looks measured.

# `mesh`

```elixir
@spec mesh(Quiddity.Part.t(), keyword()) ::
  {:ok, map()} | {:error, Quiddity.Error.t()}
```

The part as triangles, with the face each triangle came from.

Options: `:deflection` (mm, default 0.05) and `:angular` (radians, default 0.3).

# `nearest`

```elixir
@spec nearest(Quiddity.Part.t(), [number()], keyword()) ::
  {:ok, map()} | {:error, Quiddity.Error.t()}
```

The faces nearest a point, closest first.

A ray answers "straight on"; this answers "nearby", which is what naming the
feature at a coordinate wants when the coordinate came from a sensor rather
than from the model.

# `open`

```elixir
@spec open(Path.t()) :: {:ok, Quiddity.Part.t()} | {:error, Quiddity.Error.t()}
```

Reads a STEP file.

A `.step` or `.stp`, plain or gzipped. A refusal comes back as
`{:error, %Quiddity.Error{}}` with quiddity's own code — `"unreadable"` for a
file that is not there, `"broken_step"` for one the kernel read and refused.

# `parts`

```elixir
@spec parts(Quiddity.Part.t()) :: {:ok, map()} | {:error, Quiddity.Error.t()}
```

The distinct parts of an assembly: indices, names, face and edge counts, and
where each instance sits.

A file that holds one part answers one entry, which is the useful answer: a
caller that does not know whether it was handed a part or an assembly can ask.

# `pmi`

```elixir
@spec pmi(Quiddity.Part.t(), keyword()) :: {:ok, map()} | {:error, Quiddity.Error.t()}
```

The file's semantic PMI — dimensions, geometric tolerances, datums and notes —
as quiddity's versioned JSON, for one part (`part: 0`) or all of them.

This is what the part *demands*, not what it looks like: the tolerance on a
bore, the datum a face is measured from. A file without PMI answers a document
with none, which is a different answer from a file that could not be read.

# `ray`

```elixir
@spec ray(Quiddity.Part.t(), [number()], [number()], keyword()) ::
  {:ok, map()} | {:error, Quiddity.Error.t()}
```

Where a ray from *origin* along *direction* meets the part.

The answer is the hits, nearest first, each naming the face, the point, the
outward normal there and the distance along the ray — plus how far the ray was
allowed to search, because "nothing that way" needs to say how far it looked.

Options: `:far` (millimetres, default the camera's own 10 m) and `:limit`
(default 8).

# `samples`

```elixir
@spec samples(Quiddity.Part.t(), keyword()) ::
  {:ok, map()} | {:error, Quiddity.Error.t()}
```

Points spread over the surface, roughly *spacing* millimetres apart.

Each carries its outward normal and the face it is on, which is everything a
graph node needs before it is given a percept. Options: `:spacing` (mm, default
2.0) and `:limit` (default 200 000 points).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
