build notes · featuretree

featuretree: heuristic feature recovery for B-rep geometry

2026-09-28 · Dan Newcome · punkfab/featuretree (MIT) · preprint 10.5281/zenodo.22848792

Update 2026-09-29: layered recognition and a stricter gate lifted average overlap from 65% to 97.6% on the same eleven parts. See the results post. The numbers below are as first published.

A STEP file imports into a parametric CAD system as one frozen lump. The design history is gone. There is no pad depth to change and no hole diameter to edit, only a boundary representation: faces, edges and surfaces. featuretree is a heuristic feature-recovery harness for that B-rep geometry. It proposes a feature tree of sketches, pads, pockets, revolves and cuts, then checks the proposal by rebuilding it and comparing the result against the input. A tree that passes can be emitted as a native, editable model in FreeCAD, Onshape or build123d.

The check is the point. Every recovery comes back VERIFIED, PARTIAL, or refused, and a PARTIAL tree is labelled as one. A silently wrong feature tree is worse than no tree, because you would edit it.

None of this is a new idea. Automatic feature recognition has been published since the early 1990s, and commercial CAD has shipped it for decades. What's missing is an open implementation you can call from code and point at FreeCAD or Onshape, neither of which recognises features on import. featuretree is that, plus an honest account of how well it works. The preprint says so up front, and its prior-art scan is in the repo.

How it works: heuristics propose, a rebuild decides

The core heuristic is that most machined and printed parts are a 2D profile swept along an axis, plus holes and pockets. So recovery is cross-sectional:

Volume of the reconstructed NIST CTC-01 solid plotted against features applied. It starts at a base envelope 155% above the target, drops sharply around feature 30, and settles into a narrow tolerance band around the target volume by about feature 45.

That carving loop on NIST CTC-01: a base extrude 155% too big, brought down into the tolerance band over 54 operations. The verifier accepts only inside the band.

The recovered tree refers to geometry symbolically — by feature name and query, never by a kernel's face or edge IDs — so the same tree drives FreeCAD, Onshape and build123d, and a human edit made in the GUI can be read back by name.

Coverage: every distinct NIST test geometry

We ran every distinct part geometry in the NIST MBE PMI test-case models: all five Combined Test Cases (CTC-01 to 05) and all six Fully-Toleranced Test Cases (FTC-06 to 11). We used NIST's "AP203 geometry only" exports, which are exact B-rep with no annotations. These are human-designed conformance parts, meant to be hard. That is the whole geometry set rather than a sample. NIST's five Simplified Test Cases reuse the FTC geometry with simpler annotations; everything NIST publishes that we did not run is listed after the results, with the reason.

Every part, every verification detail. A part is VERIFIED only if its rebuilt tree matches the input's volume to within 0.5% and its extents to within 0.05 mm; "Fails" says which test a PARTIAL part missed. IoU (intersection over union) is an independent check that uses no Boolean operations, measured with the recogniser's own transform undone, so it is an exact comparison with the input rather than a best guess at how to line the two up. The table is generated from the result files by paper/figures/corpus_table.py, so it cannot drift from the data.

PartVerdictFailsVol. errorExtent error (mm)IoU (%)FeaturesRecovered as
CTC-01VERIFIED—0.14%0.099.81 ± 0.0754extrude Z · 14 pockets, 10 holes
CTC-02PARTIALboth62.19%267.77.4 ± 0.464extrude Y · 2 regions
CTC-03PARTIALboth41.46%132.536.1 ± 3.016extrude Y
CTC-04PARTIALboth2.43%390.030.8 ± 0.5270extrude Z
CTC-05PARTIALboth51.80%349.214.5 ± 0.8106extrude Z
FTC-06PARTIALboth31.85%98.859.6 ± 0.644extrude Y · 17 pockets, 5 holes
FTC-07PARTIALextents0.09%1.595.5 ± 0.425drafted shell ∥Y (1.0°) · 3 outline layers · 9 pockets, 8 holes
FTC-08PARTIALvolume3.56%0.095.9 ± 0.448shell ∥Z · 2 outline layers · 21 floor cutouts
FTC-09VERIFIED—0.19%0.099.70 ± 0.0580extrude Y
FTC-10PARTIALboth20.80%18.076.0 ± 0.524extrude Z
FTC-11VERIFIED—0.00%0.0100.00 ± 0.002revolve

All eleven NIST parts, original STEP in grey beside the recovered tree in teal, each pair in the same registration. The three VERIFIED pairs, CTC-01, FTC-09 (a thin perforated plate) and FTC-11 (a ring), are visually identical. The eight PARTIAL recoveries range from most of the part (FTC-06 about 39%, FTC-10 about 46%, FTC-08 about 53% as two long rails) down to very little: CTC-02 and FTC-07 come back as two flat plates, CTC-03 as a single plate, CTC-05 (a flanged base with a post) as one small block, and CTC-04, close on volume, as a compact box unlike its long thin original.

All eleven, each recovery placed exactly where its IoU was measured. The picture makes the verdicts legible in a way the numbers don't. VERIFIED means indistinguishable. PARTIAL ranges from "nearly all of it" (FTC-07, a drafted box, and FTC-08, a pan) to "a plate". CTC-04 shows why volume alone is not a test: it is within 2.43% on volume, and it is plainly the wrong shape.

Published by NIST, not runNIST setWhy
STC-06Simplifiedsame geometry as FTC-06 (confirmed: volume and extents)
STC-07Simplifiedsame geometry as FTC-07 per NIST; does not import in OpenCASCADE
STC-08Simplifiedsame geometry as FTC-08 (confirmed: volume and extents)
STC-09Simplifiedsame geometry as FTC-09 (confirmed: volume and extents)
STC-10Simplifiedsame geometry as FTC-10 (confirmed: volume and extents)
FTC-08 (tessellated)Fully-tolerancedfaceted surfaces, not exact B-rep; out of scope
HTCHole2024 model, not in the corpus download used; not evaluated
MTCModified2018 model, not in the corpus download used; not evaluated

FreeCAD 1.0 Part Design workbench. The Model tree on the left lists the recovered operations under Body: the base pad, blind0 through blind13, then pocket0, pocket1, cut2 through cut13 with their placement sketches. The 3D view on the right shows the rebuilt NIST CTC-01 part: an H-shaped plate with through-holes, two slots, a hexagonal boss, side cross-holes and embossed NIST lettering.

This is what VERIFIED means in practice. NIST CTC-01 is shown as a native FreeCAD Part Design document, built from the recovered tree. Every entry in the Model panel is a real, editable operation: the base extrude, fourteen blind holes, then pockets and cuts, each with its own placement sketch. Change a depth and the part rebuilds. FreeCAD's volume for this tree matches build123d's to within 0.3 mm³. Feature names are generated, not the designer's, because recognition recovers an equivalent history, not the history.

Getting the whole corpus through

The first full run refused three of the eleven parts outright: CTC-02, FTC-07 and FTC-10. Those refusals looked principled, since those are the ugly parts, full of threads and blends. They weren't. They came from two conservative choices in the front end, not from the limits of the feature vocabulary.

The first was the axis prefilter. Candidate axes with more than 40% non-conforming faces were thrown away before anything else happened. FTC-07 and FTC-10 carry modelled thread and blend geometry; their best axes score 55.0% and 47.7%, and the offending faces are almost entirely tori, cones and spheres. They were rejected before their cross-section was ever looked at. The threshold was measuring the presence of thread geometry, not the absence of a prismatic profile. CTC-02 failed at the next step: every sampled cross-section landed in a tapered region whose outline is ellipses and hyperbolas, the signature of slicing a cone off-axis.

The second was the cross-section. A section that splits into separate regions at every height was treated as a failure. A disjoint section does describe some parts correctly, as several parallel prisms that one sketch and pad per region recovers exactly. The IR could already express that; the recogniser never tried. We raised the prefilter to 75% and added the multi-region path:

First full runAfter the threshold change
VERIFIED33
PARTIAL58
REFUSED30
FTC-08 volume error52.6%32.6%

Loosening a heuristic was safe because acceptance is a separate stage, not because we got lucky. The rebuild-and-compare check never sees the prefilter threshold. So loosening it can only move a part from refused to PARTIAL or VERIFIED; it cannot create a false accept. That is the practical payoff of keeping recognition and acceptance apart. The heuristics can be as aggressive as you like, and the worst case is wasted compute. Had they been one scoring function, every threshold change would have been a soundness change. No false accept appeared. FTC-08, already recovering, improved by twenty points because the wider prefilter let a better axis win. Refusal is still reachable (a torus is still refused); this corpus just doesn't exercise it.

One of those three needed more. FTC-07 looks like it should be easy: it is basically a box, an outer rectangle padded and one pocket for the cavity. The multi-region path instead recovered it as two stray walls, 63% short. The verifier reported that honestly as PARTIAL, but it's not a good answer, and the reason is how the part was being looked at.

Seeing a box as a box

No single cross-section of FTC-07 says "box". Looked at from several aspects, it can't be anything else. Across its opening, every slice is two walls. Along it, the slices are the rectangular ring an open box should give: 18 of 24 are one region with a large inner loop, solid at the floor and open at the rim. So featuretree now cuts a stack of slices along each principal axis, and an axis whose slices are mostly rings, closed at one end, is an open shell.

The slices also explained why the part was never a prism. It's a molded box, and the rings grow steadily with depth: the outer wall 1.4 mm per side over 81 mm, the cavity the same. That's 1.0° of draft on every wall, inside and out, the taper that lets a part release from its mould. featuretree didn't have draft at all, so pads and pockets now take a taper angle. FreeCAD and build123d both build it natively, and a test pins FreeCAD's sign convention against build123d's, checked by flipping it and watching the test fail. FTC-07 is then recovered as a designer would draw it: a drafted pad and a drafted cavity pocket, two operations at 87% IoU. Carving the slots and bosses takes it to 95.5%, from two walls at 30%.

The outline isn't one thing either: a straight band at the floor, the drafted wall, then a rim lip. So the outline's size against depth is split into layers wherever it stops following a straight line, and each layer becomes a pad stacked on the one below. That brought FTC-07's volume error to 0.09%, inside the gate. It stays PARTIAL, correctly, on extents alone: the rim flares to 311.6 mm at its very edge, 1.5 mm wider than the lip layer. Getting it right also exposed a latent bug in the carving step. It lined the part up against its rebuilt base by matching bounding-box corners, which is only right when the base spans the part's full extent. FTC-07's rim lip overhangs its walls by 3.18 mm, so every carved feature had landed 3.18 mm off. Fixing it moved FTC-07 from 92% to 95%; no other part's result changed.

FTC-08 is the same problem the other way up. It came back as two L-brackets, 53% IoU, but it's a shallow pan that opens downward: a 10 mm flange at the open end, straight walls, and a 3.4 mm floor with 21 cutouts. The shell detector had found it along its depth, but one straight-line fit across the flange step read as an 8° "draft", and the result lost to the L-brackets. With layers it's a flange pad, a wall pad and a cavity pocket, and the floor slice hands over its 21 cutouts directly as inner loops: 3.6% short on volume, 96% IoU. Carving then over-cut it badly, which the rebuild check rejected in favour of the uncarved pan. A guard on individual carve steps is still owed; FTC-10 needs it too.

Measuring it honestly

The recogniser's own verdicts held up. The code measuring them was less trustworthy. Scoring the PARTIAL recoveries at all, which the first harness didn't do, surfaced a run of evaluation bugs:

So featuretree now measures IoU with no Boolean operations at all. It classifies 60,000 random points against both solids and reports a 95% interval. The check ships in the library as iou_check.py, with its own tests, and on the command line as step_recognize.py part.step --iou. On CTC-01 it reads 99.81% ± 0.07. The three VERIFIED figures agree with the Boolean ones, as they must for near-identical solids; the PARTIAL ones are where the Booleans failed. The takeaway is narrower than "never use Booleans," but it is sharp. A Boolean-derived metric is safe to report only where something independent has confirmed it, and it is least safe on the recoveries that failed, which are exactly the ones you are trying to measure.

The measurement is exact, not a best guess. featuretree works in the part's own frame, rotating the extrude axis onto Z, so a recovery is generally turned relative to its input. The first harness found the alignment by trying 24 orientations and lining up bounding-box corners, which could only ever give a lower bound, and a loose one: a rim lip or a missing flange shifts the corners. The recogniser now reports the exact rotation and offset it applied, and undoing them gives a true measurement. The old bounds understated most PARTIAL parts, some badly: FTC-10 was "at least 46%" and is 76%, and CTC-03 was "at least 6.5%" and is 36%. (FTC-08's bound happened to be tight.) The preprint's second version reported Boolean IoU for the PARTIAL parts; the figures here supersede it.

Design intent, checked against the designer's drawing

A VERIFIED tree rebuilds the part, but it reads like a machine wrote it. Holes are arcs inside generic pockets, equal holes are unrelated, and every size is a bare float. So featuretree now re-expresses the tree the way a designer would have drawn it: holes as holes (a revolve's bore counts), counterbores, patterns such as bolt circles, equally spaced lines and rectangles, the unit system, and the standard size each hole most plausibly is. Because it never changes geometry, it can't break a VERIFIED tree.

NIST's AP242 files make this measurable. They carry semantic PMI: every dimension, tolerance and datum, linked to the faces it applies to. That is the designer's stated intent in machine-readable form, so the inference can be scored:

PartHoles foundNominal sizesUnitsHole grouping (P / R)
CTC-017/77/7metric ✓—
FTC-0929/3027/27inch ✓0.78 / 0.78
FTC-112/22/2metric ✓—

38 of 39 dimensioned holes come back, all 36 inferred sizes agree with the drawing, and the unit system is right on all three. FTC-09 is an inch part, and its holes come back as the drills a machinist would reach for: a 5/32" drill where the drawing says .156", letter drills A and K where it says .234" and .281". Grouping is scored only on FTC-09, because NIST's Combined parts "are not intended to be fully toleranced" and their drawings never group holes. Even there, a callout group is a stand-in for pattern truth, not the truth: a designer can dimension two holes of one pattern separately. It runs as step_recognize.py part.step --intent.

Limits

Code is at punkfab/featuretree, MIT. The paper, with the prior-art scan behind its deliberate lack of a novelty claim, is at 10.5281/zenodo.22848792. Every number and image here regenerates from the commands in paper/figures/README.md.