Read Autodesk Fusion 360 .f3d / .f3z designs without running Fusion. A
pip-installable Python library and CLI, in the spirit of
ezdxf — ezf3d.readfile(path) and you're in.
Status: alpha. Reading, inspection, B-Rep traversal, analytic geometry, spline curves, tessellation, mesh export, offscreen rendering and Fusion's own cached display mesh all work today. Spline surfaces, feature-graph transpilation and simulation are on the roadmap below.
Exercised against four real designs: 42 B-Rep bodies, 99.8 MB of Shape Manager data, every file walked to its terminator with no unknown tokens. The geometry layer is checked against the format's own redundancy — for every edge reachable from a body with ordinary topology, the vertex must lie on the curve the edge names. Over 95,668 endpoints the worst miss is 2.2e-07 cm, well inside the kernel's own tolerance.
Where a design carries Fusion's own tessellation, that becomes a second, independent check: cached vertices sit within 1e-07 cm of every analytic surface ezf3d reads, and every one of the cache's edge polylines ends on a B-Rep vertex.
Fusion 360 has no headless mode — the only automation surface is a Python add-in that
runs inside a live Fusion instance. The one open project that touches .f3d at all,
jmplonka/InventorLoader, is a GPL-2.0
FreeCAD workbench: it can't run headless, it skips the design/feature streams entirely,
and it predates Fusion's switch to Zstandard-compressed ZIP entries, so it fails
outright on files saved today.
ezf3d is a clean-room MIT implementation of the format.
uv venv --python 3.13
uv pip install -e ".[dev]"ezf3d info <file> # doc type, versions, schema table, segments, size breakdown
ezf3d tree <file> # asset folders, segments, BREP inventory (.f3z: XREF graph)
ezf3d bodies <file> # per-body ASM topology census + geometry histogram
ezf3d dump <file> --out <dir> # explode the archive, decompressed
ezf3d thumb <file> --out <png> # extract the embedded preview
ezf3d raw <file> <entry> # forensic token/hex dump of any stream
ezf3d render <file> --out <png> # wireframe or --shaded, six views plus iso, --turntable
ezf3d mesh <file> # tessellate and report coverage, deviation, watertightness
ezf3d export <file> --out <stl> # STL, OBJ, glTF, GLB
ezf3d ogs <file> [--verify] # what Fusion cached, and how far it agrees with the B-Rep
ezf3d components <file> # the component tree, its bodies, and its materials
ezf3d params <file> # every parameter: name, role, unit, expression, value
ezf3d timeline <file> [--inputs] # the features in run order, what each does and drives
ezf3d sketches <file> [--place] # every sketch: points, curves, loops, where it sits
mesh, export and render take --source asm | ogs | auto: tessellate the surfaces,
read Fusion's cached mesh, or use the cache when it covers the whole body and tessellate
otherwise.
Every command takes --json for machine consumption.
import ezf3d
with ezf3d.readfile("Design.f3d") as doc:
doc.manifest.doc_type # 'Fusion Document'
doc.design.bulk.feature_counters() # Counter({'Sketch': 17, 'FilletEdgeFeature': 13, ...})
body = doc.bodies[0] # nothing parsed yet - bodies load lazily
body.model().header.kernel_release # '232.4.0.65535'
body.census().faces # 2006
body.census().analytic_only # True -> tessellable without a spline kernel
len(doc.design.objects()) # 3444 -> design objects, each with an offset and extent
design = ezf3d.model.read_design(doc.design)
design.components[0].name # 'SUCKER v2' -> and .bodies, .features
params = ezf3d.model.read_parameters(doc.design)
params.by_name()["d20"].expression # '14 mm' -> and .role, .unit, .value, .display
timeline = ezf3d.model.read_timeline(doc.design, params)
[f.kind for f in timeline][:3] # ['CylinderPrimitive', 'CylinderPrimitive', 'Sketch']
timeline.features[3].role("AlongDistance").expression # '-50 mm'
timeline.features[3].extrude.operation # 'Cut' -> and .direction
ezf3d.model.read_assignments(doc.design)[0].appearance # 'PrismMaterial-018'.f3z packages resolve their reference graph: readfile returns the root design, with
doc.linked holding the XREF'd documents and doc.package the graph itself.
.f3d is a ZIP whose entries are Zstandard-compressed (method 93 — stdlib
zipfile cannot open them). Inside:
| Path | Contents |
|---|---|
Manifest.dat |
doc type, GUIDs, {module: schema_version} table |
<Asset>[Active]/<Segment>/{Meta,Bulk}Stream.dat |
typed object graph — the parametric timeline |
<Asset>[Active]/Breps.BlobParts/*.smb, *.smbh |
ASM BinaryFile8 — Autodesk Shape Manager B-Rep (.smbh carries rollback history) |
<Asset>[Active]/OGS.BlobFolder/… |
One Graphics scene graph + pre-tessellated display mesh |
<Asset>[Active]/ProteinAssets.BlobParts/*.protein |
nested ZIP — Autodesk Protein materials |
<Asset>[Active]/Previews/small.png |
thumbnail |
Full notes live in docs/format/.
- Phase 1 — container & inspection. ✅
- Phase 2.1 — geometry & traversal. ✅ typed B-Rep walking and analytic curve and surface evaluation.
- Phase 2.2 — wireframe render. ✅ adaptive edge discretisation and a pure-numpy offscreen rasteriser.
- Phase 2.3 — tessellation & export. ✅ trimmed analytic faces, shaded rendering, STL/OBJ/glTF.
- Phase 2.4 — splines. ✅ for curves:
nubs/nurbsreading, de Boor evaluation, and the interning table. Spline surfaces are read but not yet trusted — see docs/format/unknowns.md. - Phase 2.5 — the OGS cached-mesh fast path. ✅ the scene graph and buffer descriptors, cross-validated against the ASM tessellation — which is how a hole-triangulation bug and a stale-loop bug were found.
- Phase 3 — design semantics. ✅ the meta stream is decoded and its object index
makes the design payload randomly addressable — 14,843 objects in one sample, each with
a known offset and extent.
ezf3d componentsreads the component tree, naming every body inBreps.BlobPartsexactly once;ezf3d paramsreads all 1,193 parameters of the four samples with their roles, units, expressions and values; andezf3d timelinereads the features in the order Fusion runs them — an order that is not creation order.ezf3d components --materialsadds each component's material, checked against the.proteinpackage that declares it, andezf3d timeline --inputssays what each feature drives — 478 of 686 features across the samples carry at least one parameter, and every one of 214 extrudes says whether it joins, cuts or makes a body. - Phase 4 — transpile. Fusion feature graph →
build123dsource → headless OCC regeneration, verified by geometric diff against the original bodies. In progress:ezf3d sketchesreads the profile. An extrude never names its sketch, but every point and curve record names the sketch that owns it — which places 3,217 of 3,221 entities across the samples and reaches every sketch each design has. Curves are typed by how many points they reference rather than by record size — 163 circles, 889 lines, 282 arcs — and chain into 274 closed loops. Two independent checks: aLinear Dimensionmust be the distance between two of its sketch's points (155 of 179), and an arc's stored radius and span must match the centre and endpoints it names (445 of 445, worst miss 2.2e-07 cm).ezf3d sketches --placethen recovers where a sketch sits, by matching its loops to the planar faces of the bodies they helped build — fitting the frame as a free affine map whose axes come out orthonormal without being asked to, worst departure 6.9e-13. What it cannot do is pick one: a design repeats its own shapes, so a profile fits at every instance of a patterned feature, and nothing in the geometry marks the seed. A second, non-geometric link goes further: an ASMsketch_attrib_defnames the sketch curve a B-Rep edge came from, by the identity the curve record itself carries. Those ids are scoped to the component, which a body already knows — with that, the link reaches 8,333 edges naming 1,079 of 1,334 curves across 125 of 130 sketches, and 794 of 796 closed edges name a circle where a scope-free read had 209 naming lines. Placing from those edges separates a patterned copy from its seed, which shape matching cannot, and lands 25 of 52 placements in exactly one spot.build123demission waits on tying a feature to the topology it produced —generic_tag_attrib_def, still unread. - Phase 5 — simulate. Mass properties and interference first, then
scikit-femlinear static / modal / thermal.
Writing modified geometry back into .f3d is an explicit non-goal; headless iteration
happens through the transpile path.
uv run pytest # everything, ~12 min over 100 MB of sample CAD
uv run pytest -m "not slow" # the inner loop, ~4.5 min (±20 s run to run)
uv run ruff check . && uv run ruff format --check .Tests run against real designs rather than fixtures, and check the format against its own
internal redundancy — a curve evaluated at its edge's parameter must reach the vertex, a
face's triangles must lie on the surface the face names, a face's mesh must cover its
outer loop less its holes. The slow marker covers the exhaustive sweeps that walk every
face of every body; the expensive results those sweeps share — parsed documents,
per-face tessellations, cache comparisons — are computed once per sample and reused.
MIT. See LICENSE.