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/1names its holes, pockets, slots, bosses, steps, patterns, fillets, chamfers, grooves, springs and turned profiles, with their measured sizes and positions, from quiddity; - where is it? —
ray/4,frame/2,samples/2andface/2answer in millimetres against the exact surfaces, fromhaecceity, the kernel quiddity re-exports.
{: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.62Why 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.
Summary
Functions
What this build of quiddity can do, as the versioned document it publishes.
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).
What happened to each feature between two revisions.
The evidence each feature rests on, and every family that was refused.
One face: its surface, area, centroid, normal and neighbours.
What quiddity recognised in the part.
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.
One camera frame: what each pixel of the camera landed on.
Whether the Rust engine is built and loaded in this VM.
Volume, area, centroid and — when a density is given — mass of every solid.
The part as triangles, with the face each triangle came from.
The faces nearest a point, closest first.
Reads a STEP file.
The distinct parts of an assembly: indices, names, face and edge counts, and where each instance sits.
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.
Where a ray from origin along direction meets the part.
Points spread over the surface, roughly spacing millimetres apart.
Functions
@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.
@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).
@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.
@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.
@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.
@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.
@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.
@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.
@spec loaded?() :: boolean()
Whether the Rust engine is built and loaded in this VM.
@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.
@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).
@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.
@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.
@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.
@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.
@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).
@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).