Skip to content

Fit CAD ​

fit_cad(mesh, path) turns the fitted patches of a closed triangle skin into a CAD solid: every patch that fit_primitives accepted becomes a plane, sphere, cylinder, cone or torus face trimmed by the patch boundary, a rolling-ball fillet or blend (of one radius or a radius varying along it) or a helical thread face its own feature face, a chamfer, a conic blend, a tapered or variable-pitch thread or a corner where blends meet its own face too, any other patch no primitive explains a freeform B-spline face, the faces are sewn into a solid that OpenCASCADE checks, and the solid is written as STEP. It is the last step of skin to CAD, after segment and fit_primitives, and it needs the optional OpenCASCADE backend.

python
import meshioplusplus as mp

skin = mp.repair(mp.read("part.stl"))
seg = mp.segment(skin, curvature_tolerance=1.0, min_patch_cells=20)
fit = mp.fit_primitives(seg, tolerance=2e-3)

solid, report = mp.fit_cad(fit, "part.step", return_report=True)
report["valid"], report["num_cylinder"], report["num_demoted"]
report["volume_ratio"], report["max_deviation"]   # against the skin

The turned part of skin to CAD as a CAD solid: two planes, a cylinder, a torus fillet, a cone and a spherical dome, re-meshed and coloured by face type

Unlike the faceted step writer, whose solid is one flat face per triangle, the STEP file fit_cad writes carries CYLINDRICAL_SURFACE, CONICAL_SURFACE, SPHERICAL_SURFACE and TOROIDAL_SURFACE faces a CAD tool can select, dimension and edit. A skin without segment:patch or the fit:patch_* table is segmented and fitted first with the default options; a noisy scan wants a curvature test and a min_patch_cells in the tens, so run those two yourself, or pass them to the CLI.

How the solid is built ​

  • Faces. A patch whose fit was accepted, and whose type is in types, becomes one face on its fitted surface. A patch no primitive explains is tried, in order, as a rolling-ball fillet between two analytic faces (fillets), as a helical thread face (threads; then of varying pitch or on a taper), as a chamfer or conic blend between two analytic faces (fillets) and, with freeform (the default), as a B-spline face; once the freeform faces exist, a patch between two fitted faces, one of them freeform, is tried as a rolling-ball blend against them, and a corner patch among three or more fitted faces as a vertex blend (fillets). Every other surface cell (a type left out, a freeform fit that failed, a cell with no patch) becomes planar facets, one per cell, as the faceted writer writes them.
  • Edges. Where two patches meet, the mesh edges between them form chains from one junction (a point where three or more patches meet) to the next; a closed chain is split in two. Between two analytic faces the edge is the branch of the two surfaces' exact intersection that lies within the tolerance of the chain, when exact_edges is set and one does (a plane meeting a cylinder gives a circle). Otherwise, notably where a fillet meets its wall tangentially and no intersection is stable, the edge is a B-spline through the chain, smoothed and moved onto both surfaces, kept only if it stays within the tolerance of both. Beside a facet, the edge is the mesh's own straight edge.
  • Vertices. A junction among analytic faces only is moved onto all of their surfaces; one beside a facet keeps the mesh position.
  • Solid. Each face gets parameter-space curves that are continuous around its loops (so a boundary running along a cylinder's seam or a torus's equator does not fall apart), OpenCASCADE adds the seams and repairs the faces, sews them and closes each shell into a solid, one per edge-connected piece of the skin. A patch with no boundary at all, a whole sphere or torus, is its surface's natural face.

Freeform faces ​

A patch fit_primitives left unexplained (no accepted primitive, or one with non-finite parameters) gets a B-spline surface fitted to it, so a smooth blend, a dent or a styled panel becomes one CAD face instead of hundreds of facets:

  1. A parametrisation. The patch is flattened over its best-fit plane (principal components) or, when that folds, unrolled around a cylinder: the axis is the direction its normals are most perpendicular to (the smallest eigenvector of Σ nnᵀ), the radius a least-squares circle fit of the points around it, the angle cut at the widest gap. A flattening whose triangles do not all keep one orientation is not a parametrisation and is not used. A patch that folds over both (a doubly curled one, or a band closed around an axis) is, with freeform_split (the default), split into pieces that one of them does parametrise.
  2. A height field. The offset from the plane or the cylinder is resampled on a grid over the flattened bounding box, interpolated inside the patch's triangles and continued from the nearest boundary point outside them, so the surface extends past the patch's trim.
  3. The fit. GeomAPI_PointsToBSplineSurface fits the grid (iso-parametric, C2, degrees up to freeform_degree, default 3, valid 3–8). The grid starts near √n points a side and doubles up to freeform_max_control (default 64, valid 4–1024) until every patch vertex lies within half the tolerance and every facet centre within the tolerance; a patch that never gets there, or a skin too coarse for any smooth surface to pass within it, stays faceted.
  4. The face. The surface is oriented by the facet normals and goes through the same trimming, sewing, check and demotion as an analytic face; its fit_cad:type is 6 (kFitCadBsplineType), and the report's num_bspline counts it.

freeform=False keeps every unexplained patch faceted.

Splitting curled patches ​

A patch that folds over both flattenings is halved at the area-weighted median of n · e, where e is the direction its normals spread along most (the principal axis of the area-weighted Σ nnᵀ): a cut along an iso-line of the normal, a smooth curve on a smooth surface. Each half is split into its edge-connected components, and a component that still folds is halved again, up to four times. Each piece that now flattens becomes its own freeform face under a new patch label, numbered after the largest existing one in the order of its lowest facet, so fit_cad:patch tells the pieces apart; a piece under 16 triangles, or one still folding at the fourth halving, stays faceted. The edges between pieces are approximated B-splines like any other tangent junction. The split is deterministic and needs no OpenCASCADE (detail/patch_flatten); num_split_patches counts the pieces, and freeform_split=False keeps such a patch faceted, as before.

A capped band whose radius waves along its axis: the wall, closed around the axis, is split into two freeform B-spline faces between two planar caps

Fillets, blends and threads ​

Features that no primitive explains are recognised from the patches around them, before the freeform fallback or, for a blend, after it. The fits need no OpenCASCADE (detail/feature_fit); the faces they give are then trimmed, sewn, checked and demoted like any other.

  • Rolling-ball fillets (fillets, type 7). A patch that runs between two analytic faces (the two neighbours sharing the most boundary, together at least 60% of it) is tried as a constant-radius fillet between them: each interior vertex, moved by r against or along its normal, should be a ball centre at r from both primitives. The radius minimises that misfit over a log-spaced scan of [tolerance / 2, diagonal] refined by golden section, for a convex (centres inside) and a concave (centres outside) ball. The face is the pipe of that radius around the spine where the two surfaces, each offset by r towards the centres, meet (GeomFill_Pipe around the GeomAPI_IntSS branch nearest the centres, kept whole and periodic when the centres go most of the way round it). It covers a straight rounded edge, a swept fillet along a curved edge (the foot of a leaning boss, along an ellipse) and a ring. A constant-radius fillet between coaxial surfaces is a torus, which fit_primitives already fits; this stage only sees what it left unexplained.
  • Fillets of varying radius (fillets, type 7, counted in num_variable_fillet). When no one radius explains the patch, each interior vertex gets its own ball radius, and the radii become a law along the rail, the patch's boundary on the first surface: the parameter of a vertex is where its own ball touches that surface, a Chebyshev series (a Fourier series round a closed rail) of the lowest degree up to 8 that explains the radii within a quarter of the tolerance. The face is a B-spline through cross-sections of the ball family: along the rail, the ball touching both surfaces at the law's radius is solved for in the plane through the rail point, and each section is the arc of the ball's characteristic circle from one foot to the other, widened by 15% each way, the sections running a tenth of the rail past each open end so the face trims inside its surface.
  • Blends beside freeform faces (fillets, type 7, counted in num_blend). Once the freeform faces exist, a patch between two fitted faces, at least one of them freeform (plane–B-spline or B-spline–B-spline), is tried as a rolling ball against those surfaces, with the radius found per vertex and fitted as a law exactly as above, constant or varying; a freeform patch can become a blend this way. The neighbours are taken as the freeform stage left them, so the result does not depend on the order the patches are tried in. A ball resting on a nearly flat stretch of a fitted face touches it where its normal happens to point, which wanders far more than the face does, so the balls' centres and radii, not their feet, set the face: smooth laws are fitted through the solved centres (judged across the spine, as a wavering rail stretches the arc length along it) and the arcs ride on them. A blend stays tangent to its fitted neighbours, which lie within half the tolerance of the skin, so a vertex on the blend's own boundary may lie the whole tolerance from it.
  • Helical thread faces (threads, type 8). A patch beside an accepted cylinder or cone is first tried as a straight-flank thread about its axis: the ruled helicoid z = c + k θ + t ρ (k the pitch over 2π, t the flank's slope), one least-squares solve once each vertex's angle is unwrapped over the patch's edges; the face is a B-spline through a grid on it. A patch that is not such a flank, or that has no cylinder or cone beside it, is tried as a helical surface of any profile. Without an axis, the screw comes from the patch itself, as the screw motion whose velocity field is most nearly tangent to it (Pottmann and Randrup, Rotational and helical surface approximation for reverse engineering, Computing 60, 1998): the smallest eigenvector of a normal-weighted 6 × 6 moment, reduced to 3 × 3, refused when a plane, cylinder or sphere leaves more than one screw nearly free, or when the advance vanishes (a surface of revolution). The axis and the advance are then refined together by Levenberg-Marquardt on the residual of the profile: each vertex, with the screw's advance taken off its height, lands in the plane through the axis, and the profile is a Chebyshev series of the lowest degree up to 8, ζ(ρ) or ρ(ζ), within 0.4 of the tolerance. A straight profile is a flank; any other (a rounded root or crest) is counted in num_rounded_thread. A trapezoidal or ISO thread then comes back as cylinders (root and crest) and two helical flanks, and a thread with rounded roots and crests, with no cylinder anywhere, as four helical faces.

Every patch must span at least half a turn to be a thread (less of a helical surface is as well something else). A candidate face must keep every patch vertex within half the tolerance and every facet centre within it, as a freeform face must. Edges beside a fillet, a blend or a thread face skip the exact intersection, which is slow and unstable between tangent or long B-spline surfaces, and go straight to the approximated B-spline. num_fillet and num_helical count the faces, and of them num_variable_fillet, num_blend and num_rounded_thread the new kinds; fillets=False and threads=False leave such patches to the freeform fit.

A boss leaning on a round plate, its foot rounded by a fillet swept along an ellipse: the fillet is one rolling-ball face between the plate's plane and the boss's cylinder

A block whose edge is rounded by a ball shrinking from 0.3 to 0.2 along it: one fillet face of varying radius between two planes

A bar whose wavy top and leaning front are freeform B-spline faces, the edge between them one rolling-ball blend

A rod with a trapezoidal thread: root and crest are cylinders, both flanks helical faces, the shallow end caps facets

A rod whose thread has rounded roots and crests and no cylinder anywhere: four helical faces, found from the screw each patch fits

Chamfers, conic blends, vertex blends and tapered threads ​

Since v17.2.0 (roadmap 6.3) four more kinds of patch get their own faces. Each is tried only where every fit above has failed for that patch, so a skin they do not concern builds the same solid, to the byte, as before; their fits are in detail/feature_fit too.

  • Chamfers (fillets, type 10, num_chamfer) and conic blends (fillets, type 7, num_conic_blend). A patch between two analytic faces (the two neighbours sharing the most boundary, together at least 60% of it) that no rolling ball and no thread explains is tried as a blend of the edge between them with a cross-section other than the rolling ball's circle. Its rails are the patch's boundary edges across from each neighbour; its cross-sections lie in the planes normal to the edge the two surfaces would meet along (n₁ × n₂, through each point of the first rail), from the first rail to where that plane cuts the second. When every interior vertex lies within 0.4 of the tolerance of its section's chord, the patch is a chamfer, of constant or varying width (a chamfer between two planes of varying width is a twisted ruled surface, a constant one a plane, and a chamfer coaxial with a cylinder a cone: those two stay the primitives fit_primitives fits). Otherwise each section is taken as a rational quadratic from rail to rail through the apex where the two surfaces' tangent lines meet, the vertex's weight w = b₁ / (2 √(b₀ b₂)) read off its barycentric coordinates in that triangle, and a weight law of the lowest degree (Chebyshev, Fourier round a closed rail) whose conics pass within 0.4 of the tolerance of every vertex makes it a conic blend: an elliptic, parabolic or hyperbolic section tangent to both faces, of legs that may differ and change along it. The face is a B-spline interpolating the sections, each widened by 15% and carried a tenth of the rail past an open end.
  • Tapered and variable-pitch threads (threads, type 8, num_tapered_thread, num_variable_pitch_thread). A straight flank beside a cone is the same ruled helicoid as a flank of a straight thread (with an apparent pitch), so it is built as before and reported tapered. A flank whose straight helicoid fails is refitted with one more column, z = c + k θ + q (θ − θ₀)² + t ρ, a pitch changing linearly along it, and kept when the pitch changes by more than the tolerance across the patch. A patch no constant screw explains is refitted as a helical surface of any profile on a generalised screw, its advance g(θ) = k θ + q (θ − θ₀)² and its profile moved out by τ g(θ) as it advances, a thread cut on a cone whose radius grows by τ per unit length: τ starts at a neighbouring cone's half-angle and is refined with the axis, the advance and q by the same Levenberg-Marquardt profile fit. A trapezoidal pipe thread then comes back as cones and two tapered flanks, a thread whose pitch grows as cylinders and two variable-pitch flanks, and a rounded one on a taper as four helical faces, the rounded root and crest tapered.
  • Vertex blends (fillets, type 9, num_vertex_blend). After the freeform and blend stages, a patch that is unexplained or freeform, whose boundary is one closed loop around three or more fitted faces (each met along one run) and creased nowhere (no facet across it turns more than 20°), is tried as the corner where blends meet: a plate surface (GeomPlate_BuildPlateSurface) through its vertices, tangent (G1) to each neighbour's surface along the loop, approximated by a B-spline. It is kept when it passes the freeform face's test and its normal stays within 3° of every neighbour's along the loop. This covers a setback corner, three blends of different radii or sections meeting at one vertex; a corner of three equal rolling balls is a sphere, which fit_primitives already fits.

Segmentation is the condition: a conic blend is tangent to both its faces and its curvature varies across it, so no curvature tolerance of segment isolates it alone; label such a skin's patches yourself (segment:patch, from the CAD faces it was tessellated from, say), as the fixtures behind the tests do. A chamfer meets its faces at creases and segments by angle.

A block whose edge is cut by a chamfer of growing width: one chamfer face, a twisted ruled surface, between two planes

A rounded box scaled unequally: twelve conic edge blends and eight vertex blends at its corners, between six planes

A pipe thread: a trapezoidal thread cut on a cone, its root and crest cones and both flanks tapered helical faces

Checks and demotion ​

A face is not kept when OpenCASCADE's B-rep check rejects it, when sewing leaves an edge of it free, when its boundary cannot be built, or when it strays: the solid is re-meshed and a face with a point further than tolerance from the skin, or a patch with a skin point further than that from the solid, fails. A failed patch is demoted to facets and the solid is rebuilt; after max_demotion_rounds rounds (default 8) every remaining patch is faceted, which is always a valid solid for a closed skin. So a feature the fitter cannot explain, or a face the kernel cannot trim, comes back faceted, never silently wrong, and num_demoted counts the features dropped. MESHIOPLUSPLUS_LOG_LEVEL=debug logs why each patch was demoted.

tolerance defaults to three times the largest fit:patch_tolerance of an accepted patch, so it follows the scale fit_primitives worked at; with no accepted patch it is 1e-3 of the bounding-box diagonal. The re-meshing uses a chordal deflection of a quarter of the tolerance unless mesh_deflection says otherwise.

Output and report ​

The return value is the solid re-meshed by OpenCASCADE as one triangle block, with points welded where faces share them exactly, and Int64 cell data fit_cad:face (the B-rep face), fit_cad:type (0 a facet, else the fit:type value: 1 plane, 2 sphere, 3 cylinder, 4 cone, 5 torus; 6 a freeform B-spline, 7 a rolling-ball fillet, blend or conic blend, 8 a helical thread face, 9 a vertex blend, 10 a chamfer) and fit_cad:patch (the segment:patch label, -1 for a cell with no patch). With return_report=True the report holds:

KeyMeaning
validthe solid passes OpenCASCADE's B-rep check
tolerancethe tolerance applied
volume, skin_volume, volume_ratiothe solid's volume, the skin's enclosed volume and their ratio
max_deviationthe symmetric Hausdorff distance between the skin and the re-meshed solid
num_faces, num_plane … num_torus, num_bspline, num_fillet, num_helical, num_faceted_facesB-rep faces in all, by type (freeform B-splines, fillets and thread flanks included), and planar facets
num_variable_fillet, num_blendof the fillet faces, those whose radius varies along them, and the blends beside a freeform face
num_rounded_threadof the helical faces, those whose profile is not a straight line (a rounded root or crest)
num_vertex_blend, num_chamfervertex blends and chamfers (v17.2.0)
num_conic_blendof the fillet faces, the conic blends (v17.2.0)
num_tapered_thread, num_variable_pitch_threadof the helical faces, those cut on a taper and those whose pitch varies (v17.2.0)
num_split_patchespieces that freeform patches folding over every flattening were split into
num_patches, num_demotedpatches in the fit table, and accepted ones demoted to facets
num_exact_edges, num_approx_edgesedges between analytic faces built as exact intersections and as B-splines
num_solidssolids written, one per piece of the skin

The STEP file is AP214 in millimetres. Its header timestamp is pinned and its product is named meshio++ fit_cad, so the same input writes the same bytes; the bytes do depend on the OpenCASCADE version. The work is serial, so nothing depends on the thread count.

The solid as NURBS ​

solid.info["brep"] (C++ FitCadResult::mBrep, a BrepInfo) holds the solid with every face converted to NURBS (BRepBuilderAPI_NurbsConvert): the surfaces, their trimming loops and the edges with the faces they bound, as a STEP read gives them. Every face of the solid is there, trimmed, and every trimming curve lies in its surface's parameter range. When the exact conversion does not deliver that (OpenCASCADE 7.9 throws on a trimmed cylinder or one wound several turns round, leaves a face that already was a B-spline without a surface, and can stop a converted surface short of its trims), the solid is kept as it is and each surface and edge that is not a B-spline is approximated by one in its own parameters, within 1e-6 of the bounding-box diagonal, so the trimming curves stay valid; planes and lines stay exact. Writing it as Kratos CAD JSON (mp.write("part.cad.json", solid), to-cad --cad-json) makes a scanned part Kratos IGA geometry.

Getting OpenCASCADE ​

Open CASCADE Technology is licensed under the GNU LGPL 2.1 with the additional exception in its OCCT_LGPL_EXCEPTION.txt. meshio++ stays MIT: the backend is off by default and bring-your-own (never vendored or downloaded), linked dynamically, and absent from the PyPI wheels, the lean WASM build and the release CLI binaries; the full WASM artifact links it statically, under a mixed licence it states. A build that enables it links an LGPL library; distributing that build carries the LGPL's obligations for OpenCASCADE, not for meshio++. OpenCASCADE 7.6 or later is supported (7.6 is Ubuntu 24.04's package; the CI leg tests it).

bash
sudo apt-get install libocct-foundation-dev libocct-modeling-data-dev \
    libocct-modeling-algorithms-dev libocct-data-exchange-dev libtbb-dev
CMAKE_ARGS="-DMESHIOPLUSPLUS_WITH_OCCT=ON" pip install meshioplusplus --no-binary meshioplusplus

From source, build/configure.sh --with-occt (or -DMESHIOPLUSPLUS_WITH_OCCT=ON) finds OpenCASCADE through its CMake config; point OpenCASCADE_DIR or CMAKE_PREFIX_PATH at an install outside the default prefixes, and make its libraries findable at run time as for any shared library. Debian's packages export toolkit targets that link TBB, hence libtbb-dev.

Without the backend, fit_cad raises NotImplementedError (C++ Unsupported) naming -DMESHIOPLUSPLUS_WITH_OCCT=ON; there is no pure-Python fallback, because trimming, sewing and checking surfaces is a CAD kernel's work. meshioplusplus._fit_cad.has_occt(), fit_cad_has_occt() in C++, mio_has_occt() in C and the language surfaces below say in advance whether a build has it.

CLI ​

sh
meshioplusplus to-cad IN OUT.step [--curvature-tolerance C] [--min-patch-cells N] \
    [--fit-tolerance T] [--tolerance T] [--no-exact-edges] [--types plane,cylinder,...] \
    [--mesh-deflection D] [--max-demotion-rounds N] [--no-freeform] [--freeform-degree P] \
    [--freeform-max-control N] [--no-freeform-split] [--no-fillets] [--no-threads] [--mesh-out FILE] [--cad-json FILE] [--json] [--quiet]

A skin without segment:patch is segmented with --curvature-tolerance and --min-patch-cells, and fitted with --fit-tolerance (relative, as in fit-primitives), before the solid is built. --mesh-out also writes the re-meshed solid, --cad-json the solid as Kratos CAD JSON; --no-freeform, --freeform-degree, --freeform-max-control and --no-freeform-split are the freeform options, --no-fillets and --no-threads the feature ones. Both CLIs exit with 2 when the solid fails the B-rep check, as check does for a failed gate.

sh
meshioplusplus to-cad part.stl part.step --curvature-tolerance 1 --min-patch-cells 20 --fit-tolerance 2e-3

Pipeline ​

json
{"Op": "FitCad", "Path": "part.step", "Tolerance": 0.0, "ExactEdges": true,
 "Types": ["plane", "cylinder"], "MeshDeflection": 0.0, "MaxDemotionRounds": 8,
 "Freeform": true, "FreeformDegree": 3, "FreeformMaxControl": 64,
 "FreeformSplit": true, "Fillets": true, "Threads": true}

The step writes the solid to Path when given, and the mesh continues as the re-meshed solid; it reports Valid, VolumeRatio, MaxDeviation, NumFaces, NumBspline, NumFillet, NumHelical, NumVariableFillet, NumBlend, NumRoundedThread, NumVertexBlend, NumChamfer, NumConicBlend, NumTaperedThread, NumVariablePitchThread, NumSplitPatches, NumFacetedFaces and NumDemoted. A Tolerance or MeshDeflection of 0 is automatic; an empty Types facets everything.

Other languages ​

c
mio_fit_cad_opts opts;
mio_fit_cad_opts_init(&opts);
opts.types = (1u << MIO_FIT_PLANE) | (1u << MIO_FIT_CYLINDER);
opts.freeform = 1;             /* the default; freeform_degree, freeform_max_control */
mio_fit_cad_report report;
mio_fit_cad_report_ex report_ex;  /* v17.2.0: num_vertex_blend, num_chamfer, ... */
if (mio_has_occt()) {
    mio_mesh* solid = mio_fit_cad_ex(fitted, "part.step", &opts, &report, &report_ex);
}
fortran
solid = fit%fit_cad(step_path='part.step', valid=ok, num_demoted=ndropped, stat=ierr)
julia
has_occt() && (r = fit_cad(fit; step_path="part.step"); r.valid)
r
if (mio_has_occt()) cad <- mio_fit_cad(fit, step_path = "part.step")

The C report carries num_bspline, num_fillet, num_helical, num_variable_fillet, num_blend, num_rounded_thread and num_split_patches, and mio_fit_cad_ex's extended report (mio_fit_cad_report_ex, v17.2.0, as mio_fit_cad_report has no room left) num_vertex_blend, num_chamfer, num_conic_blend, num_tapered_thread and num_variable_pitch_thread; Fortran's, Julia's and R's results carry all of them (freeform, freeform_degree, freeform_max_control, freeform_split, fillets and threads are their options; a zero-filled C struct keeps the last three off, mio_fit_cad_opts_init turns them on). The MCP server's fit_cad tool takes the same options plus curvature_tolerance, min_patch_cells, fit_tolerance and cad_json_output_path, and reports occt_available; without the backend it answers with the named error and occt_available: false. In the browser, the full WASM artifact links OpenCASCADE and runs fitCad(mesh, {stepPath, ...}), with the same options and report in camelCase and the solid's B-rep as info; the lean artifact throws by name.

Not supported ​

  • Blends of a section that is no conic, a chamfer or conic blend beside a freeform face (only analytic neighbours are tried), and a corner patch whose loop meets a neighbour twice or at a crease: they come back freeform or faceted.
  • Threads whose depth varies along them, or whose pitch varies other than linearly, and a tapered or variable-pitch thread with no cylinder or cone beside it that is also too far from one screw for the axis to be found from the patch itself: freeform or faceted, as is a thread patch spanning less than half a turn.
  • A whole thread as one patch: a thread surface whose profile spans the full pitch (one patch over root, flanks and crest) has no single trimmed face to be; segment it at its creases or with a curvature test first.
  • Fitting a STEP or IGES model: those are read as their own NURBS B-rep, not refitted.

Released under the MIT License.