xcaf-pmi
Turn the GD&T in an OpenCascade document (.xbf) into an AP242 STEP file with semantic PMI,
the form that CAD, CAM and inspection tools read, and an AP242 STEP back into an OpenCascade
document, with more of its PMI than OpenCascade's own reader keeps.
If your application builds parts with OpenCascade and keeps its dimensions and tolerances in an
XCAF document (text-to-cad's cadgen does), xcaf-pmi reads them out and
haecceity (in the quiddity package) writes them into
the STEP as AP242, in one step.
No GD&T in your document yet? xcaf-pmi only carries what is there. To add it, give the STEP to
Specify (pip install specify-core, or
in the browser): it recognises the features and asks only
what is missing.
Install
pip install xcaf-pmi # Python 3.11+, brings OCP and quiddity
Make an AP242 STEP
xcaf-pmi to-step part.xbf -o part.step
xcaf-pmi part.xbf -o part.step # the same: an .xbf (or .xml) input goes to STEP
part.step has the part's geometry, face names and colours, its GD&T as semantic PMI, and a drawn
callout for each item. haecceity reads the file back before it is written and reports any
difference.
What it prints, on stderr:
not carried over: …: something in the document that has no place in the PMI, or that could not be checked (an edge whose number on the STEP written cannot be checked, say).--strictfails on these instead, writing nothing.read back differently: …: haecceity's read-back found the file says something other than what was sent. Also fails--strict.note … written as general tolerance ISO 2768-m, ornote … kept as text: …: how a note that states a general tolerance was read (see General tolerances).read finding: …,writer finding: …,output finding: …: information about how something was read or written (e.g. a free-text note kept as semantic text, or a datum target described as the CAx-IF practice asks where AP242's own rules disagree). Nothing is lost.
--part N carries only that part's GD&T; by default every part with PMI of its own has it
written on it.
Advanced: --step geometry.step writes the geometry alone (no PMI), --haecceity part.pmi.json
the PMI as haecceity's JSON, --json found.json the GD&T as read, in OCCT's terms, and
--presentation part.presentation.json (with -o) the document's own callouts and views.
Exit status, both ways: 0 written; 1 failed (an input that cannot be read, a part the file has
not, haecceity's refusal, an output that cannot be written); 2 a usage error; 3 --strict found
something not carried over, and nothing was written.
Make an XCAF document from an AP242 STEP
xcaf-pmi to-xcaf part.step -o part.xbf
xcaf-pmi part.step -o part.xbf # the same: a .step or .stp input goes to XCAF
part.xbf has the part's geometry, names and colours as OpenCascade reads them, and the STEP's
PMI as XCAF GD&T on its faces: dimensions with their tolerances, limits, fits, modifiers and
qualifiers; geometric tolerances with their magnitudes, modifiers and datum systems; datums
(also those no tolerance refers to) and datum targets; notes; an ISO 2768 general tolerance, as a
note of the part ("ISO 2768-mK", see General tolerances); material and
density; user-defined attributes; and each item's drawn callout where the STEP has one. --part N takes only one
part's PMI from a STEP with several (by default, every part's).
What it prints, on stderr:
not carried over: …: something in the STEP's PMI that XCAF has no form for (a thread, a surface texture, a general tolerance other than an ISO 2768 class, a composite frame, the end of a projected tolerance zone, an item placed only on supplemental geometry …).read finding: …: how haecceity read the STEP: what it repaired (OCCT's malformed density unit, say), read leniently, or could not read. Nothing it reads is dropped without one. A finding of kindfallbacksays haecceity refused a part, or did not read an item, and OpenCascade's own reading of it was taken instead.faces: … matched by entity(oredges: …): the STEP's faces or edges are numbered differently from OpenCascade's; they were matched by the STEP entity each came from (faces by their geometry where that fails, one to one). A face or edge that matches none, or more than one, is listed as not carried over and its items are kept unanchored, never placed on a guess.
What XCAF cannot hold is kept on the document, as haecceity's JSON on the item's XCAF label
(the part's, for an item with none), with the state of the item and of the faces it is on when it
was kept. to-step restores it where the document still states the same: an item edited since
is written as the document now has it, and what was kept for it is listed as not carried over.
Other XCAF readers do not see it: to them it is an attribute they do not know. What the round
trip STEP → XCAF → STEP still loses is listed, never dropped silently: items on surfaces
OpenCascade reads beside the solid, edges whose number haecceity gives cannot be checked, and
items haecceity's writer refuses.
Compared with OpenCascade's own GD&T reader (STEPCAFControl_Reader with GD&T on), on every
test file to-xcaf gets every category at least as right, and keeps the notes and materials
OpenCascade's reader drops: where haecceity refuses a part or does not read an item (on NIST's
STC-06, what lies on a shape aspect that identifies no face), to-xcaf takes OpenCascade's
reading of it. Facts right of those stated, OpenCascade's reader / to-xcaf:
| what the files state | OpenCascade | to-xcaf |
stated |
|---|---|---|---|
| NIST's 7 PMI test models, against NIST's expected PMI | 590 | 611 | 738 |
7 of Specify's outputs and draftwright's spindle, against haecceity's own reading of them (what to-xcaf keeps, not whether it is right) |
171 | 224 | 232 |
| cadgen's plate, both exports, against cadgen's document | 44 | 55 | 66 |
python tests/gaps.py prints them by file and category.
from xcaf_pmi import to_step
step = to_step("part.xbf") # AP242 STEP bytes; the document may also be bytes
open("part.step", "wb").write(step)
step, report = to_step("part.xbf", report=True) # or an open XCAF document
for line in report["unmapped"]:
print("not carried over:", line)
print(report["quiddity"]["differences"]) # haecceity's read-back check: [] when all is kept
The other way:
from xcaf_pmi import to_xcaf, save
doc = to_xcaf("part.step") # an OCP TDocStd_Document; the STEP may also be bytes
save(doc, "part.xbf")
doc, report = to_xcaf("part.step", report=True)
print(report["unmapped"]) # what XCAF has no form for (kept for to_step)
print(report["findings"]) # haecceity's findings on reading the STEP
to_step(..., strict=True) raises xcaf_pmi.NotCarriedOver (its unmapped lists what) instead
of leaving anything out, and haecceity's refusals raise quiddity.PmiError. In a document of
several parts, each part with PMI of its own (GD&T, notes, material, manufacturing process, or
what to-xcaf kept) has it written on its own part of the STEP, the parts matched to quiddity's
by name and number of faces; part= takes only one. A part's items on another part's faces or datums are left out and
listed. read, export_step and to_haecceity are the steps on their own. read and to_step also accept an XmlXCAF .xml document.
What is carried over
- Datums, including point, line, rectangle, circle and area datum targets (an area as XCAF holds it, the target's shape, or the faces the target is on).
- Dimensions: sizes (diameter, radius, thickness …), linear distances and angles, with ± tolerances, limits, ISO 286 fits (e.g. ⌀10 H7) or as basic dimensions, and their modifiers and qualifiers.
- Geometric tolerances: all fifteen kinds, with their datum references (modifiers and common datums too), a diameter zone, maximum or least material condition, a maximum value, a runout zone's angle, all around, all over, and XCAF's other modifiers (free state, tangent plane, statistical, line element, common zone …).
- Notes, on the part, on faces, or on a dimension or tolerance. A note has one owner in the PMI: one also on something else is listed as not carried over for that.
- General tolerances (ISO 2768), stated as a note of the part: see below.
- Material, with its density stated in g/cm³. OCCT's own STEP writer gets that unit wrong (gram³·cm²), so readers ignore its density; this route writes it correctly.
- Manufacturing process, as named data on the part: see Manufacturing process.
- Names: your item names (e.g.
thickness,hole-position) become the PMI's ids, and your face names are kept in the STEP, so the PMI points at faces by name. - Drawn callouts: each item gets a callout in haecceity's layout, linked to it as the CAx-IF practice asks, so CAD readers that show graphical PMI show it.
Not yet:
- Projected tolerance zones. AP242 needs to know where the projection ends, and XCAF holds only its length, so these are listed as not carried over.
- Callout positions and saved views. The callouts in the STEP are laid out afresh. The
document's own callouts (where it places them, on their annotation planes, with their text
points) and its saved views (camera and the items each shows) can be written out as
haecceity's presentation JSON with
--presentation part.presentation.json(with-o;report["presentation"]from Python). quiddity 0.4 cannot yet write that layout into the STEP in place of its own.
Tested on text-to-cad's pmi_plate: the AP242 made this way has the same datums, dimensions and
tolerances, on the same faces, as cadgen's own AP242 export. It adds the two notes cadgen's STEP
leaves out, a stable id for each item, and the material's density in a unit readers accept.
NIST's STEP File Analyzer (5.41) reads the same semantic PMI from both; on this route it also
finds the density valid (OCCT's export fails with "the length unit exponent should be -3"), no
duplicate datum system, and every callout as graphical PMI (none on cadgen's export, whose
callouts carry no names).
General tolerances
XCAF has no general tolerance, so a document states one as a note. A note on the part (not on a face or an item) whose whole text is one of these wordings is written as the part's general tolerance (ISO 2768-1's linear class, and ISO 2768-2's geometric class where stated), not as a note; case and spacing do not matter:
| wording | general tolerance |
|---|---|
ISO 2768-m (or -f, -c, -v) |
ISO 2768-m |
ISO 2768-mK (or H, L) |
ISO 2768-mK |
ISO 2768-1 m |
ISO 2768-m |
GENERAL TOLERANCES ISO 2768-m (also General tolerances: ISO 2768-m) |
ISO 2768-m |
Tolerances per ISO 2768-f |
ISO 2768-f |
ISO 2768-mK is the canonical wording: the general tolerance a note states is written with it.
to-step reports each such note (note … written as general tolerance ISO 2768-mK). A note that
says anything more (ISO 2768-m unless otherwise stated, DIN ISO 2768-m), names another
standard (ISO 22081's general specifications, which haecceity has no general tolerance for; DIN
7168), states an ISO 2768-1 class with a geometric class (ISO 2768-1 mK), is on a face or an
item, or is one of two such notes on a part, stays a note; where it names ISO 2768, ISO 22081
or a general tolerance, to-step says why.
The other way, to-xcaf writes a STEP's ISO 2768 general tolerance as a note of the part in the
canonical wording (or uses the STEP's own note of it, as Specify writes one), and keeps the
tolerance on that note, so STEP → XCAF → STEP gives the same general tolerance and notes. Edit
the note and to-step writes the general tolerance it now states.
Manufacturing process
A code-first model (cadgen, build123d, or another XCAF writer) declares its process as a string
in TDataStd_NamedData on the part's label, under manufacturing_process:
from OCP.TCollection import TCollection_ExtendedString
from OCP.TDataStd import TDataStd_NamedData
TDataStd_NamedData.Set_s(part_label).SetString(
TCollection_ExtendedString("manufacturing_process"),
TCollection_ExtendedString("3D printed: FDM"),
)
The options are machined, 3D printed: FDM, 3D printed: resin, 3D printed: powder,
3D printed: metal, sheet metal, moulded, and cast (case does not matter). to-step
writes one specify process UDA property set on that part's PRODUCT_DEFINITION, which
Specify reads as the file's process. An invalid value is listed as not carried over and
fails --strict.
to-xcaf reads a valid manufacturing_process item from any part-level attribute set into
that key, ignoring case, spaces and hyphens in the item key. A fresh declaration writes no
suggestion marker. An imported default.manufacturing_process = 'true' marker is kept as its
own named data key and written back with the process, so an accepted suggestion round-trips.
Limits
- A document given as bytes is read through a temporary file (OCCT's in-memory BinXCAF reader is unusable from Python).
- Where haecceity refuses a part,
to-xcafhas only OpenCascade's reading of its GD&T: no notes, no kept extras. (quiddity 0.4.4 reads every test file; earlier 0.4 releases refused NIST's STC-06 and STC-09 over their attributes' duplicate or empty ids.) - quiddity 0.4 states no STEP entity for an edge:
to-xcafchecks haecceity's edge numbers against the edges the STEP's PMI is on, and reports those it cannot check (two of NIST STC-10's). - Where OpenCascade reads a part as a solid with surfaces beside it (some NIST models), PMI on those surfaces is carried into XCAF but not back to STEP.
More
- The PMI JSON format (
haecceity.pmi/1): its reference and JSON Schema. - To add manufacturing intent to a STEP that has none (fits, datums, tolerances, by answering a
few questions): Specify (
pip install specify-core), or in the browser. - Developing xcaf-pmi:
pip install -e '.[test]'andpytest, which includes the end-to-end tests through haecceity.tests/make_plate.pybuilds a test document through OCCT's own GD&T API, andtests/fixtures/holds text-to-cad'spmi_plate(MIT), NIST's CTC-01 with NIST's expected PMI, and three of Specify's outputs (seetests/fixtures/README.md). The other test files are fetched once from pinned commits (tests/files.py) into~/.cache/xcaf-pmi(or$XCAF_PMI_CACHE); tests needing them are skipped when offline.
Apache-2.0.
Metadata
Release files for xcaf-pmi 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| xcaf_pmi-0.1.0.tar.gz | 89.7 kB | With provenance | Details for xcaf_pmi-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| xcaf_pmi-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for xcaf_pmi-0.1.0-py3-none-any.whl |
Total release size: 157.4 kB
Release files / xcaf_pmi-0.1.0.tar.gz
| Download URL | xcaf_pmi-0.1.0.tar.gz |
|---|---|
| Size | 89.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3598947d3ea19aaee9ef98752c1a1350e41a6bce33ab4d692838b548a71ad61b
|
|
BLAKE2b-256 checksum How to use checksums |
f4c4ca64aca246e1e660673602e621f0bbe2feeb89e0d80fe2b1854a66b693e4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.
Transparency logRelease files / xcaf_pmi-0.1.0-py3-none-any.whl
| Download URL | xcaf_pmi-0.1.0-py3-none-any.whl |
|---|---|
| Size | 67.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5ea6993df3d64277da35c37c660b020078c2f31f531fb50be2477c6c59626d99
|
|
BLAKE2b-256 checksum How to use checksums |
882c6daace3ba06dd2af7e875b192614a3b7c647ac7a82e3960780b3c6758bb3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.
Transparency logSecurity for xcaf-pmi 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for xcaf_pmi-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 7, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.