Skip to main content

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). --strict fails 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, or note … 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 kind fallback says haecceity refused a part, or did not read an item, and OpenCascade's own reading of it was taken instead.
  • faces: … matched by entity (or edges: …): 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-xcaf has 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-xcaf checks 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]' and pytest, which includes the end-to-end tests through haecceity. tests/make_plate.py builds a test document through OCCT's own GD&T API, and tests/fixtures/ holds text-to-cad's pmi_plate (MIT), NIST's CTC-01 with NIST's expected PMI, and three of Specify's outputs (see tests/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)

Source distribution for xcaf-pmi 0.1.0
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)

Table of built distributions (wheels) for xcaf-pmi 0.1.0
File Interpreter ABI Platform Actions
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page