mesh2cad turns an STL back into CAD: a STEP file, a CadQuery script, or a parametric OpenSCAD file. Yesterday's post was about how badly it first did on six real Original Prusa i3 MK3S parts, and how 0.9.0 got the simplest one (a 48-triangle plug aligner) out as an editable extrusion.
The second part, the endstop block, was the frustrating one. The STEP came back exact (15 planes, 2 cylinders, Hausdorff 0.017 mm), but the parametric export refused it. That's a bad place to be: the geometry is understood, but you still can't open a script and change the hole.
0.10.0 fixes that for this part. This post is how, and where it still stops.
Why one extrusion wasn't enough
The 0.9.0 exporter looks for one axis where the whole part is a 2D outline pushed straight along that axis. For the endstop block it reported, along each axis, the faces that were tilted relative to it:
- along X, 4 tilted faces (largest 65 mm²)
- along Y, 4 tilted faces (largest 73 mm²)
- along Z, 3 tilted faces (largest 73 mm²)
So no single axis works. The cross-section changes along the part in every direction.
But that's what a lot of printed parts look like when you draw them in CAD: you sketch the top view and extrude it, then sketch the side view and cut with it, then the front. The solid is the intersection of its silhouettes.
Intersecting the silhouettes
So that's what 0.10.0 tries. For each axis it takes the fitted planar faces you can see looking along that axis (not the side walls, which are edge-on), projects them onto the perpendicular plane and unions them into one outline. The wall planes are used to snap that outline back to straight edges. Each outline is extruded over the part's extent, two or three of those prisms are intersected, and whatever extra material is still inside the result has to be explained by a hole, a straight cut or a planar clip.
For the endstop block the result is:
-
outline_z, eight points, extruded 14 mm along Z -
outline_x, five points, extruded 13.56 mm along X -
outline_y, five points, extruded 9 mm along Y - an Ø3.3 mm through-hole along X, with an Ø6.2 mm counterbore 8 mm deep from the −X end
- one planar clip: a face tilted to all three axes, which none of the silhouettes can produce, written as a half-space cut with its normal and offset
Here's the start of the OpenSCAD file it writes (straight from mesh2cad convert endstop-block.stl --openscad endstop.scad):
/* [Extrusions] */
// length along Z
extrude_z_len = 14.0;
// start along Z
extrude_z_0 = -15.0;
// length along X
extrude_x_len = 13.56;
...
/* [Holes] */
// diameter (axis X)
hole_1_d = 3.3; // [0.2:0.1:9]
hole_1_y = -11.0;
hole_1_z = -11.0;
// counterbore diameter
hole_1_cbore_d = 6.2; // [3.3:0.1:9]
// counterbore depth from the -X end
hole_1_cbore_depth = 8.0; // [0.2:0.1:13.6]
and the model itself is plain CSG:
difference() {
intersection() {
translate([0, 0, (extrude_z_0 - 0.02)])
linear_extrude((extrude_z_len + 0.04))
polygon(outline_z);
translate([(extrude_x_0 - 0.02), 0, 0])
rotate([90, 0, 90])
linear_extrude((extrude_x_len + 0.04))
polygon(outline_x);
// ... and the Y silhouette
}
// through-hole, counterbore, planar clip
}
The CadQuery script is the same thing with .intersect() and .cut(). Both have 15 parameters.
Two details took longer than they should have:
- The 0.02 mm overlap. If the intersected prisms end exactly on the same plane, the shared end faces are coincident and OpenSCAD/CGAL sometimes produces a non-manifold result. Each prism is extended by 0.02 mm past its ends, but only the intersected ones; the outer extent stays at the true end, so the solid doesn't grow.
- A plane that thought it was a sphere. The short counterbore floor got fitted as a sphere under some tolerances. A "sphere" whose vertices mostly lie on one axis-aligned plane is now read as that plane.
Does it actually match?
Before writing either script, mesh2cad executes it (CadQuery) or renders it (OpenSCAD CLI) and compares the result to the mesh. If the sampled deviation is over max(4·tol, 0.1 mm) or the volume error is over 1%, nothing is written and you get the reason.
On the endstop block:
| check | value |
|---|---|
| max deviation, script vs mesh | 0.0421 mm (limit 0.1 mm) |
| volume, mesh | 968.72 mm³ |
| volume, CadQuery script | 967.55 mm³ (0.11% low) |
| conversion time, STEP + both scripts | about 8 s on my box |
The 0.11% is mostly the chordal error of the two round holes in the STL: the mesh's holes are polygons, the script's are true cylinders.
Changing the hole
This is the point of the whole thing, so I tested it. I changed one parameter at a time in the generated CadQuery script, re-ran it, and compared the volume change with the analytic value:
| edit | volume change | expected |
|---|---|---|
hole_1_cbore_d 6.2 → 6.6 |
32.170 mm³ | π/4 · (6.6² − 6.2²) · 8 = 32.170 mm³ |
hole_1_d 3.3 → 3.5 |
4.273 mm³ | a 4.0 mm long ring of that size |
The counterbore matches to the last digit. The through-hole only removes material over the 4.0 mm of the block that's left past the counterbore at that height (the Y silhouette slopes there), which is what the number says. The test suite does the same kind of check on synthetic parts for both exporters, clean and at 0.05 mm noise.
What it still refuses
Two of the six real Prusa parts now export parametrically. The other four still don't, and the message says why: "part has unfitted (faceted) regions". They have small edge rounds and belt-profile patches that aren't planes, cylinders, cones, spheres or a fillet between two planes, and exact sewing needs every region fitted. Their STEP output is a valid solid, but faceted, not something you'd edit.
Also refused in 0.10.0, each with its own message:
- a boss on an extruded outline
- a countersink, chamfer or draft on an extrusion
- a rounded edge that isn't one of the four vertical corners of a rectangular base
- a stepped hole that isn't a counterbored through-hole, or a hole that doesn't go through
- more than four faces tilted to all three axes
- a recess that isn't a straight cut along X, Y or Z
I'd rather it refuse than write a script that looks parametric and is quietly wrong.
Trying it
pip install "mesh2cad[cadquery]"
mesh2cad convert part.stl -o part.step --cadquery part.py --openscad part.scad
It also runs as an MCP server (mesh2cad mcp), so an assistant can call convert, to_cadquery and to_openscad and then edit the parameters for you. The real-part benchmark is python benchmarks/real_parts.py; it downloads the Prusa parts (GPL-2.0, not vendored) and prints the tables above.
Next is the edge rounds on the other four parts, since that's what blocks all of them. If you have a simple mechanical STL it gets wrong, an issue with the file (or a link to it) is the most useful thing you can send.













