FreeCAD Import/Export module for Rhino .3dm files. Geometry is read and
written as true NURBS — control points, weights, degree, and knot vectors are
preserved exactly, with no tessellation.
FreeCAD 1.1+ required. rhino3dm ≥ 8.32 recommended (≥ 8.0.0 works, but importing Rhino-authored trimmed surfaces needs the trim-topology read API added in the 8.32 line — see Requirements).
Rhino-authored trimmed Breps are read using rhino3dm's official Brep
trim-topology API (BrepFace.Loops / Trims / Edges, rhino3dm ≥ 8.32) and
reconstructed as trimmed faces sewn into a solid/shell, entirely through
FreeCAD's own Part/OCCT API (no pythonOCC/OCC.Core dependency). Files produced
by this module's own exporter, and older rhino3dm, use the legacy reconstruction
path automatically.
| rhino3dm type | FreeCAD result |
|---|---|
| Brep (trimmed multi-face solid) | Trimmed Part faces sewn to a Solid/Shell; analytic faces optionally native (see below) |
| Extrusion (cylinder/box/pipe) | Native Part::Cylinder etc. (analytic parameters read directly) |
| NurbsSurface | Part::Feature with BSplineSurface |
| NurbsCurve | Part::Feature with BSplineCurve wire |
| SubD | Smooth limit-surface Mesh::Feature per SubD (the SubD's subdivided Catmull–Clark limit — complete and closed); the file's Rhino control-net meshes are auto-hidden. An exact NURBS-patch reconstruction also exists but is experimental and not offered as a separate import type — see Notes on SubD |
| Mesh | FreeCAD Mesh::Feature (NURBS not preserved by the originating app) |
Native analytic primitives. With Import: native primitives on (default),
Brep faces flagged by Rhino as planar / cylindrical / conical / spherical are
rebuilt on native OCCT Plane/Cylinder/Cone/Sphere surfaces — the analytic
parameters are fitted from the face geometry (rhino3dm exposes only the type
flag, not the parameters) and each fit is accepted only within tolerance,
otherwise the face falls back to NURBS. This gives cleaner solids that behave
better under booleans, fillets and draft. (Needs numpy in FreeCAD's Python;
without it every face uses NURBS.)
| FreeCAD surface type | 3DM output |
|---|---|
| BSplineSurface | NURBS surface — control points, rational weights, knots copied exactly |
| Plane | NURBS surface via OCC toNurbs() |
| Cylinder | Rational NURBS surface (exact); native r3.Cylinder optional (see preferences) |
| Cone | Rational NURBS surface (exact); native r3.Cone optional |
| Sphere | Rational NURBS surface (exact); native r3.Sphere optional |
| Torus | Rational NURBS surface (exact); native r3.Torus optional |
| Other analytic surfaces | NURBS via OCC toNurbs() / toBSpline() |
Whole objects are traversed correctly: selecting an App::Part, a PartDesign Body, or a group exports all the geometry nested inside it. The final solid of a PartDesign Body is exported once.
Trimmed surfaces on export. rhino3dm's Python bindings expose no API to author Brep trim topology, so a trimmed FreeCAD face cannot yet be written as a true trimmed Rhino Brep. The exporter instead writes the underlying (untrimmed) NURBS surface and then its trim boundary as separate NURBS curves — both the surface and its edges are preserved, and the face can be re-trimmed on the Rhino side. Full trimmed-Brep export awaits trim-authoring support in rhino3dm.
NURBS fidelity. Rational geometry (cylinders, cones, spheres, tori, and any circular edges) is written with its true control-point weights, so circles stay circular on re-import. Periodic (closed) surfaces and curves are converted to clamped form first, and circular/arc edges are written as exact degree-2 rational arcs rather than polyline approximations.
The native-primitives behaviour is controlled by a Preferences option (see below; it is off by default because the bounded-NURBS path round-trips trim boundaries more reliably).
Open Edit → Preferences → Import-Export → ImportExport 3DM:
| Option | Default | Effect |
|---|---|---|
| Export Cylinder / Cone / Sphere / Torus as native primitives | Off | When on, writes analytic surfaces as exact rhino3dm primitives (smaller files). When off (default), they are written as exact bounded rational NURBS, which reconstructs trimmed faces more reliably on re-import. |
Import: native primitives (ImportNativePrimitives) |
On | Rebuild planar/cylindrical/conical/spherical Brep faces on native OCCT analytic surfaces (fitted, with per-face tolerance check and NURBS fallback). Turn off to import every face as NURBS. Requires numpy. |
| Import: create groups | On | Mirrors each named .3dm group as an App::DocumentObjectGroup so an object's faces and boundary curves stay together. |
| Import: try to make shell/solid | Off | After collecting a group's surfaces, attempt Part.makeShell() (and makeSolid() if closed); falls back to individual surfaces. |
This is the option to reach for if you don't want the grouped tree layout.
- On (default): every named group in the
.3dmbecomes anApp::DocumentObjectGroup. Useful when each exported object carries its own group, but it can produce a group that wraps a single same-named object. - Off: no container objects are created. Surfaces are placed directly under
the top-level
Partand keep their own names; boundary curves are labelled{groupName}_{curveName}so the association stays visible without nesting.
Set it from the preferences page, or from the Python console:
FreeCAD.ParamGet(
"User parameter:BaseApp/Preferences/Mod/ImportExport_3DM"
).SetBool("ImportCreateGroups", False)rhino3dm must be installed into the Python interpreter FreeCAD actually
loads from (its own bundled Python, or the matching per-user site). Importing
Rhino-authored trimmed surfaces needs rhino3dm ≥ 8.32 (the trim-topology
read API); older versions still import untrimmed surfaces, curves and the
module's own exported files. The wheel is compiled, so it must match FreeCAD's
Python version and platform — FreeCAD 1.1 uses CPython 3.11 (cp311).
Install (or upgrade), then confirm the version and the trim API in FreeCAD's Python console:
import rhino3dm as r3; print(r3.__version__)
b = r3.BoundingBox(r3.Point3d(0,0,0), r3.Point3d(1,1,1)).ToBrep()
print(hasattr(b.Faces[0], "OuterLoop")) # True from 8.32 on/Applications/FreeCAD_1.1.app/Contents/Resources/bin/python \
-m pip install --upgrade "rhino3dm>=8.32"Find FreeCAD's Python via the console (import sys; print(sys.executable)),
then /(that)/python -m pip install --upgrade "rhino3dm>=8.32".
- Windows / modern Linux / Apple-Silicon macOS — a cp311 wheel of rhino3dm ≥ 8.32 is available; the trim-import path works.
- Linux — the ≥ 8.32 wheel needs a modern glibc (
manylinux_2_28, i.e. Ubuntu 20.04+ / RHEL 8+); older distros cap below the trim API. - Intel macOS — the current universal2 wheel is tagged macOS 15+, so trimmed import is only available on macOS 15 (Sequoia) or newer; earlier Intel Macs fall back to untrimmed import.
- The trimmed-Brep import path uses only FreeCAD's own OCCT (no pythonOCC/OCC.Core needed). numpy — bundled with FreeCAD — is used for the native-primitive fit; without it, import falls back to NURBS surfaces.
cd ~/Library/Application\ Support/FreeCAD/Mod # macOS
# or
cd ~/.local/share/FreeCAD/Mod # Linux
git clone https://github.com/KeithSloan/ImportExport_3DM.gitRestart FreeCAD. The importers and exporter appear automatically in
File → Open / File → Import / File → Export.
Not yet listed — install manually for now.
- Import:
File → OpenorFile → Import— select a.3dmfile and choose3DMfrom the format dropdown. - Export:
File → Export— choose3DM.
Version and progress information is printed to the FreeCAD Report View during import and export. The module version and rhino3dm version are printed at module load time.
A companion Blender extension exports NURBS geometry directly to .3dm:
This enables a lossless Blender → 3DM → FreeCAD NURBS pipeline:
Blender NURBS surface / Surface Psycho patch
↓ Blender_Export_3DM
.3dm file
↓ ImportExport_3DM (File → Open)
FreeCAD Part::Feature (exact BSplineSurface)
↓ KS_CurvesWB Import commands (optional)
Editable NurbsSurfaceFP / NurbsCurveFP objects
Test .3dm files are in testCases/ and UserTestCases/. The current set
covers trimmed planar faces (Cone, PolysurfCylinder, Stein), trimmed
spherical faces with holes (Stein), and analytic cylinder/cone solids.
Contributions of further test cases are welcome (must be authored in native
Rhino) — in particular a trimmed free-form NURBS face with a curved-boundary
cut/hole, a trimmed cylinder/cone wall (hole bored through the side), a face
with multiple holes, and a trimmed torus.
Additional Rhino sample files: https://www.rhino3d.com/download/opennurbs/6/opennurbs6samples · Rhino API reference: https://developer.rhino3d.com/api/rhinocommon/
- SubD (imported as its smooth limit surface). A subdivision surface is a
coarse control cage whose smooth limit is the shape it converges to under
Catmull–Clark subdivision. rhino3dm's Python bindings expose the SubD topology
and
Subdivide()but noToBrep/ToNurbs, so the module subdivides the SubD a couple of levels and imports the resulting limit surface as a single smoothMesh::Featureper SubD — complete, closed and faithful to the Rhino shape. The Rhino control-net meshes carried in the same file are detected and their visibility is switched off on import (relabelled “SubD control net (hidden)”) so the tree stays tidy but nothing is discarded. This is what the plain 3DM importer does with every SubD. - SubD as exact NURBS patches (experimental, not a menu option). A separate
reconstruction (
importSubD.makeSubD) emits, for every regular interior quad (valence-4 smooth corners), the exact uniform bicubic B-spline limit patch of its 4×4 control-net one-ring — polesB = M·P·Mᵀwith the standard uniform-B-spline→Bézier matrixM, verified against rhino3dm's ownSurfacePointto ~1e-15 — as nativePartB-spline faces, plus the control-net cage drawn as quads by a lightweight pivySoIndexedFaceSetview provider. It is geometrically exact where it exists, but incomplete: extraordinary regions (n-gon faces, valence≠4 vertices, boundaries, creases) have no single clean patch, so the shell is left open/holey and OCCT tessellates it coarsely — the result looks blocky and the cage shows through. Because there is no case today where that is preferable to the smooth limit surface, it is not registered as a user-facing import type; the code is retained (import3DM._SUBD_AS = "subd") for future completion via Stam eigenbasis evaluation at the extraordinary points, which would close the shell and make it a clean, downstream-operable (e.g. CAM) NURBS target. - NURBS and FEM. FreeCAD's FEM workbench is mesh-based, which discretizes away the exact NURBS geometry. The meshless alternative for NURBS is isogeometric analysis (IGA) — using the NURBS basis directly (open-source: G+Smo, PetIGA, tIGAr, Nutils). FreeCAD has no native IGA today, and trimmed multi-patch models remain a research area, so an export-to-IGA companion is the realistic path rather than native support.
- SubD surfaces now import as a single smooth limit-surface
Mesh::Featureper SubD (the subdivided Catmull–Clark limit — complete and closed), under the default 3DM import type. The Rhino control-net meshes carried in the file are auto-hidden (relabelled “SubD control net (hidden)”) rather than cluttering the tree. Previously a SubD produced only a diagnostic message. New moduleimportSubD.py. (On theSubDbranch.) - An exact NURBS-patch + control-net cage reconstruction is also present
(
importSubD.makeSubD) but is experimental and not registered as an import type — it is geometrically exact on regular quad regions yet incomplete at extraordinary vertices (holes → blocky tessellation). Retained for future completion (Stam evaluation → watertight NURBS → CAM-ready).
- Fixed a hard crash (FreeCAD segfault) when importing some Rhino-authored
trimmed surfaces. Sewing the reconstructed faces called
Part.Solid()on an open shell (a single trimmed face, or a shell containing a face with a hole); on such a shell OCCT raises a C++Standard_Failurethat a Pythonexceptcannot catch, which aborted FreeCAD.Part.Solid()is now attempted only when the sewn shell is genuinely closed — open shells are kept as shells. Closed solids (e.g. PolysurfCylinder, Cone) are unaffected. Surfaced by a Rhino-authored free-form trimmed face with a curved-boundary hole.
- Rhino trimmed surfaces now import as trimmed faces. Multi-face Breps are
read via rhino3dm's official Brep trim-topology API (≥ 8.32) —
BrepFace.Loops/Trims/Edges— and reconstructed as trimmedPartfaces (outer loop + inner-loop holes, singular apex/pole trims skipped) sewn into a solid or shell. Previously a Rhino trimmed solid imported as a wireframe cage. - Native OCCT reconstruction — no pythonOCC. The trim path is built on
FreeCAD's own
Part/OCCT API, so it no longer depends on the conda-onlyOCC.Core(pythonOCC) package. - Native analytic primitives (
ImportNativePrimitives, default on): planar, cylindrical, conical and spherical Brep faces are rebuilt on nativePlane/Cylinder/Cone/Spheresurfaces (parameters fitted from geometry, per-face tolerance check, NURBS fallback, and a guard against trimming to the complementary region). - Automatic path selection. Files with real trimmed Breps use the new path; the module's own exported files and older rhino3dm use the legacy reconstruction unchanged.
- Export now traverses containers. Selecting an
App::Part, a PartDesign Body, or a group exports all nested geometry (previously such selections could produce a nearly empty file). The whole selection list is exported, not just the first object. - Rational weights preserved. NURBS surfaces and curves are written with their true control-point weights and created as rational where needed, so cylinders, cones, spheres, tori and circular profiles no longer come back with straight/flattened edges.
- Exact rational arcs. Circular and arc edges are exported as exact degree-2 rational arcs instead of high-degree polynomial approximations.
- Periodic geometry handled. Closed surfaces/curves are converted to clamped form before writing.
- Import: degenerate trimmed faces fall back to the untrimmed surface. When trim reconstruction produces an invalid, zero-area face, the importer repairs it or falls back to the (valid, correctly curved) untrimmed surface rather than leaving a collapsed face. (import3DM 0.1.13)
- Test cases kindly supplied by Jonne Neva (cheezebreeze), EdWilliams, Sven
- Keith Sloan
- Chris Grellier
GNU Lesser General Public License v2.1 — see LICENSE.