FerroCAD

FerroCAD is a Rust reimplementation of FreeCAD's
C++ App core, designed to be a drop-in replacement for the FreeCAD Python package. The
Python import namespace stays FreeCAD; the project/distribution is ferrocad.
Status
| Track | What it is | State |
|---|---|---|
| Engine | the pure-Rust document model behind the FreeCAD API |
A1/B1/A2/B2 done; conformance 160 passing (Document.py 87/137, ~18 real gaps). Persistence (C) and the MVP surface (D) outstanding. |
| App shell | the bite-gpui window driving the engine through embedded CPython |
S1 done: decorated window, inspector, property editor, Python console, status bar. S2–S6 planned. |
| Distribution | how it ships | done: v0.1.3, twenty-one crates on crates.io, three self-contained artifacts (AppImage / .dmg / zip) plus cargo install. |
| Workbenches | reusing upstream workbenches | cargo xtask mods fetches Draft; loading is best-effort and stops on the FreeCADGui stub (Workbench, addWorkbench, …) and the missing FreeCAD.addImportType. |
This is a proof of concept, not a product. It exists to validate the single
riskiest assumption of the rewrite plan: that a Python script written against
FreeCAD's public App API can be served by a Rust implementation instead of the
C++ one, without changing the script.
Roadmap. The headless engine MVP is defined in docs/direction/mvp-path.md, the
interactive window in docs/direction/app-shell-vision.md, and the full milestone
record in docs/record/milestones.md. Next: lock the MVP demo, then build the
FreeCADGui workbench host (Workbench, commands, workbench lifecycle) so an unmodified upstream
workbench can load.
Install
Download a prebuilt app
Self-contained builds from the
latest release. Each one
bundles CPython, the FreeCAD facade and the Draft workbench, so no system
Python is needed:
| Platform | Download |
|---|---|
| Linux (x86_64) | ferrocad-x86_64.AppImage |
| macOS (arm64) | FerroCAD.dmg |
| Windows (x86_64) | ferrocad-windows-x86_64.zip |
The Linux AppImage is a single executable:
&&
Install from crates.io
cargo install ferrocad builds the same self-contained app from source. It needs a
Rust toolchain; install one with rustup if you do not have it:
|
The binary embeds the Python payload and unpacks it on first run (see
Embedded Python). The Python package
(pip install ferrocad, the FreeCAD API without a GUI) is not on PyPI yet.
macOS: the
.appis ad-hoc signed, so a downloaded DMG needsxattr -dr com.apple.quarantine /Applications/FerroCAD.app(or right-click → Open) on first launch. Windows: the zip is unsigned, so SmartScreen may warn on first run.
The idea
flowchart TD
S["hello_freecad.py<br/>(unchanged FreeCAD API)"]
M["python/FreeCAD<br/>facade"]
P["crates/ferrocad_py<br/>ferrocad (PyO3)"]
K["crates/ferrocad_core<br/>pure-Rust core"]
S -->|"import FreeCAD"| M
M --> P
P --> K
hello_freecad.pyuses only the public FreeCAD API. The same file is intended to run against upstream FreeCAD.python/FreeCAD/is a drop-in module that speaks that API; it knows nothing about C++ or Coin. Sinceferrocadis just a document-object model (no "application"), the facade adds the small App-level layer it lacks: a document registry,ActiveDocument, unique-name allocation, andVersion.crates/ferrocad_py/exposes the model as real#[pyclass]types via PyO3 (importable asferrocad).crates/ferrocad_core/is the pure-Rust implementation behindferrocad_py: no Python, no UI, just the object model.
Workspace layout
FerroCAD is a Cargo workspace (edition 2024). Crate status:
| Crate | Role | Status |
|---|---|---|
crates/ferrocad |
the base app: runs the general edition (cargo run -p ferrocad) |
published (app binary) |
crates/ferrocad_types |
base value types: quantities, units, geometry (Vector3, Placement, …) |
primary |
crates/ferrocad_theme |
the visual-language token table: linear draw tokens and packed-sRGB UI tokens, with the round-trip helpers | primary |
crates/ferrocad_graphics |
the neutral graphics primitives: the draw contract (DrawPacket/RenderQueue/Geometry/…), camera/viewport, and CPU picking |
primary |
crates/ferrocad_core |
pure-Rust model: properties, documents, recompute DAG (re-exports ferrocad_types) |
primary |
crates/ferrocad_geom |
the geometry seam: opaque Shape, History/ElementMap, the GeometryBackend trait |
new |
crates/ferrocad_occt |
the OCCT backend: implements GeometryBackend (box/fuse/cut/fillet/place) |
new — needs OCCT 7.8+ |
crates/ferrocad_part |
the Part workbench: registers Part::Feature and Part::PropertyPartShape |
new — needs OCCT 7.8+ |
crates/ferrocad_part_py |
the Part Python module (makeBox, Shape); the composition root that links OCCT |
new — needs OCCT 7.8+ |
crates/ferrocad_py |
PyO3 extension over ferrocad_core (module ferrocad) |
primary |
crates/ferrocad_inventor |
the Coin/Inventor scene core: node AST, state accumulator, mesh compiler, events (re-exports ferrocad_graphics) |
primary |
crates/ferrocad_stage |
the modern declarative CAD presentation: ShapeItems on a Stage, tools and a TransformGizmo, compiled straight to ferrocad_graphics (no scene graph) |
primary |
crates/ferrocad_wgpu |
the wgpu backend: pipelines, dynamic uniforms, MSAA, offscreen target, readback | primary |
crates/ferrocad_inventor_py |
the pivy-compatible coin facade (scaffolding, surfaced as pivy.coin) |
new |
crates/ferrocad_stage_py |
the modern-stack Python surface (surfaced as ferrocad.stage) |
new |
crates/ferrocad_gen |
generated PyO3 skeleton bindings from the .pyi model (M3c) |
useful — drives the codegen tests |
crates/ferrocad_widgets |
reusable bite-gpui widgets (window chrome, field, console) |
published |
crates/ferrocad_viewport |
the bite-gpui viewport: InventorViewport (image path), the navigation cube (NaviCube), the sample scene |
primary |
crates/ferrocad_gpui |
library: reusable app shell (run/run_with + HostConfig), boots CPython |
primary |
crates/ferrocad_parametric |
edition binary: the base app + the Part workbench (cargo run -p ferrocad_parametric) |
new — needs OCCT 7.8+ |
crates/xtask |
build tasks: cargo xtask bundle stages a distribution |
tool |
The GUI crates (ferrocad_viewport, ferrocad_widgets, ferrocad_gpui, ferrocad) are excluded from
default-members so cargo build/cargo test stay fast; ferrocad_gpui additionally carries the PyO3
auto-initialize feature, which conflicts with the extension-module feature of the extension crates.
Build & run
# full demo: headless script → .FCStd + PNG + STL
# open the saved file in the viewer
# or reopen and render/export it without a window
--render <png> boots the edition's app script with GuiUp == 0, asks its display
hook for the tessellated document shapes, and draws one offscreen frame with the
same Stage → WgpuRenderer path the window uses. It needs a wgpu adapter (a
software Vulkan ICD is enough) but no display server; add --size WxH to choose
the framebuffer, and --script <py> to build the model first (the script runs after
the edition's sample document is created). --open <file> loads a saved document
at boot instead of the sample: with a window it is the basic .FCStd viewer (model
tree, properties and 3D view follow the file), and with --render it renders the
file. --stl <path> writes the same tessellation as a binary STL (it needs no GPU
and can be combined with --render). The generated examples/toy_headless.png is
the Toy edition's slab rendered this way; examples/headless_part.py is the Part
demo script, and examples/headless_gear.py builds the two-gear train shown in
examples/gear_headless.png.
The Parametric edition needs OCCT 7.8. Fetch the 7.8.1 prefix CI uses into
.occt/prefix once (recipe in
docs/architecture/occt-bundling.md);
.cargo/config.toml then supplies OpenCASCADE_DIR, OCCT_INCLUDE_DIR and
LD_LIBRARY_PATH automatically, so no exports or .env file are needed.
Expected output:
FreeCAD version : 0.1.3
Active document : HelloWorld
Document label : HelloWorld
Object count : 1
- Box (App::FeaturePython) label='Hello, FreeCAD'
obj.Description : Created by the Rust core
Properties : ['Description']
Documents : ['HelloWorld']
Active after close: None
OK
Tests:
&& PYTHONPATH=python PYTHONPATH=python PYTHONPATH=python PYTHONPATH=python PYTHONPATH=python &&
Regenerate the M3c skeleton (crates/ferrocad_gen/src/lib.rs, committed):
Run upstream tests against our FreeCAD (conformance harness, M3d):
Documentation
The Rust crates carry Diátaxis-structured rustdoc — a tutorial, how-to guides, an explanation of the design, and the generated item reference:
The crate page of ferrocad_core (crates/ferrocad_core/src/lib.rs) reads as a tutorial and an
explanation with How-to/Reference sections; it is rendered on docs.rs when published. The
per-module pages are the reference.
The product surface is Python (import FreeCAD) and deliberately mirrors upstream FreeCAD, so
the behaviour reference is FreeCAD's own. What FerroCAD adds is generated and verified:
docs/user/ renders one page per implemented workflow by running it
against the facade, and docs/python-api/ is a signature reference read
from the live modules. CI runs tools/userdocs.py --check and tools/api_docs.py --check, so neither
can drift from the code. Narrative notes live in the repository docs/ (index).
An mdBook edition of both — tutorials, how-to guides, reference, explanation — is
assembled by tools/build_book.py (--build renders it with
mdBook) and published at
https://ferrocad.github.io/docs/ from the
ferrocad/docs repository, whose Pages workflow
renders a shallow copy of docs/ and tools/.
A Sphinx/mkdocstrings site is the natural next step once a Python doc build can run in CI; until
then rustdoc is the verifiable source of truth for the Rust side.
Packaging
pyproject.toml builds the distribution ferrocad with
maturin: it compiles crates/ferrocad_py into the top-level module
ferrocad and ships the pure-Python FreeCAD/FreeCADGui packages from python/. A user still
writes import FreeCAD.
The Rust library crates (ferrocad_core, ferrocad_py, ferrocad_widgets, ferrocad_gpui) and the
app (ferrocad) publish to crates.io, and the wheel publishes to PyPI. A v* tag drives both
.github/workflows/crates.yml (publish order, payload staging) and
.github/workflows/release.yml (the platform artifacts). The
abi3/extension-module notes, the publish order, and the staging commands are in
docs/architecture/releasing.md and docs/architecture/distribution.md.
cargo install ferrocad produces a self-contained binary: crates/ferrocad embeds the workspace
python/ (and, when published with workbenches, mods/) and unpacks it on first run, because Cargo has
no data-file install step. The publish staging (cargo xtask stage-payload) keeps those directories out
of the crate source; they are copied in only for cargo publish and removed after.
Application artifacts (AppImage, .dmg, Windows zip) are built by
packaging from the payload staged by cargo xtask bundle.
Tagging v* runs .github/workflows/release.yml,
which verifies the tag against the workspace version, runs the tests, builds the
three artifacts with the pinned Python runtime, and publishes a GitHub Release.
The same tag runs .github/workflows/crates.yml,
which publishes the library crates to crates.io (needs a CARGO_REGISTRY_TOKEN
secret).
Embedded Python (running scripts from Rust)
The shell library (crates/ferrocad_gpui) embeds CPython and drives it from Rust. It passes
four headless #[gpui::test]s, one of which embeds the interpreter, imports a Python module, and
round-trips a click through a Python callback back into a bite-gpui element
(cargo test -p ferrocad_gpui). The base app (crates/ferrocad) is a thin binary over it
(cargo run -p ferrocad). This is the Blender-style model the M5 UI track builds on: Python
declares the UI/workbench, Rust owns the process and renders.
Note the layering: ferrocad_core is pure Rust and does not embed Python; the embedding lives
in the shell library. Workbench Python is driven by the shell, while the FreeCAD facade talks to
the ferrocad extension the ordinary ways (import).
Python-only scripts & file loading
A plain workbench-style script that uses only the public FreeCAD API runs today —
hello_freecad.py is one, and examples/file_roundtrip.py demonstrates the file path:
create a document, doc.saveAs(path), FreeCAD.open(path), and read the objects/PropertyLength
back. The on-disk format is FerroCAD's own JSON under a .FCStd name; it is not yet upstream's
.FCStd/zip and importing real Part geometry is still out of scope.
PYTHONPATH=python
The Python bridge
crates/ferrocad_py builds the PyO3 extension ferrocad (M3b). ferrocad.Document /
ferrocad.DocumentObject are native #[pyclass] types over
Arc<Mutex<ferrocad_core::Document>>, built with abi3 and
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1, because this sandbox runs CPython 3.14, which is
newer than PyO3 0.25 officially targets. FreeCAD/__init__.py imports it unconditionally
and reports FreeCAD.backend == "ferrocad".
PyO3 is a hard build dependency on the CPython headers (CI installs python3-dev).
An earlier M0 ctypes/C-ABI fallback was removed: it was not a second transport for the
same core but a second, narrower model, and every capability it had is available through
PyO3. See docs/architecture.md for the call-path diagrams
and the decision.
See docs/direction/rewrite-strategy.md for the direction and
docs/viewport/coin-bridge-reuse-assessment.md
for the underlying analysis.
What is implemented vs. not
Implemented in ferrocad_core / ferrocad_py (the real model, not just a stub):
- a full
Quantity/Unitsystem: 8-dimension signatures, an internal unit table (SI base + derived + imperial + prefixes), an expression parser (fractions, scientific notation, compound units,pi/sin/cos/tan, feet-inchesN'(expr)"), arithmetic,Value/UserString/getValueAs/toNumber. Property/PropertyContainer,StringHasher/StringID.Document/DocumentObjectwith default naming, apetgraphdependency graph, topological recompute order, andremoveObject.- transactions (
open/commit/abort+undo/redo), anObservertrait, and an expression parser/evaluator withObject.Propertynames andrecompute().
Exposed through the FreeCAD facade (enough for the hello world and a workbench-style script):
- documents:
newDocument,closeDocument,getDocument,listDocuments,ActiveDocument, unique-name allocation,Name/Label. - objects:
addObject(default naming),getObject,removeObject,Objects,CountObjects,Name/Label/TypeId/Document,recompute. - properties:
addProperty,get/setPropertyByName, dynamic attribute mapping (obj.Foo),PropertiesList,getTypeIdOfProperty. - a single Python bridge, reported as
FreeCAD.backend("ferrocad").
Not implemented (deliberately out of scope for this milestone):
- the geometry kernel (
Part::Boxetc. produce no shape), - upstream's
.FCStd(zip) format — persistence is FerroCAD's own JSON, App/Guisplit, Coin3D, Qt,FeaturePythonscripting callbacks,- thread-safety guarantees beyond a coarse global mutex.
Next: the MVP app shell
The headless engine is the foundation; the next chunk is a single interactive window
that drives it — a developer tool with five pillars: open/close/load/save, undo/redo,
a DOM-like inspector, a property editor, and a Python console. It is one
bite-gpui window embedding CPython, calling the engine only through the public FreeCAD
API, so commands, console input and property edits all take the same path (and every edit is
one more undoable transaction). The ferrocad_gpui spike already proves the
Python-declared-UI ↔ bite-gpui round-trip headlessly.
The plan, architecture and slices (S1 shell skeleton → S2 lifecycle + commands → S3 inspector
→ S4 property editor → S5 console → S6 polish) are in
docs/direction/app-shell-vision.md. The 3D viewport now docks into
the shell's pane as an image-path presenter (readback → RenderImage) drawing a sample
Inventor scene; the full scenix/wgpu viewport and the overlay/NaviCube work are in
docs/viewport/inventor-renderer.md.
S1 is implemented (crates/ferrocad_gpui): a titled bite-gpui window that boots an
embedded CPython interpreter, creates a sample document, and shows a model inspector, a
read-only property editor, a 3D viewport (the sample Inventor scene, image path), a Python
console and a status bar — all
driven live through the FreeCAD API (python/ferrocad_shell). Run it with
cargo run -p ferrocad (needs a display); the headless #[gpui::test] is the CI-proof path.
Repository layout
Cargo.toml Cargo workspace (edition 2024)
LICENSE LGPL-2.1-or-later (the `license` field is the SPDX id)
screenshot.png the shell screenshot above
pyproject.toml maturin packaging (distribution `ferrocad`)
hello_freecad.py the milestone script (public FreeCAD API only)
examples/file_roundtrip.py saveAs / open a document (file loading)
run.sh / build.sh convenience wrappers
python/FreeCAD/ drop-in module: __init__.py (facade)
Base.py core data types re-exported from ferrocad (Vector/Matrix/Rotation/…)
Units.py units facade (Quantity)
Console.py minimal Print* logging facade
python/ferrocad.abi3.so built PyO3 bindings (module `ferrocad`, gitignored; for the wheel/`build.sh`, not the app)
python/ferrocad_gen.abi3.so generated skeleton bindings, M3c (gitignored)
python/FreeCADGui/ console-mode Gui stub
python/ferrocad_spike/ Python declarative UI spike module
crates/ferrocad_core/ pure Rust core: quantities, properties, DAG, tx/observers/expr
crates/ferrocad_py/ PyO3 bindings over ferrocad_core (module `ferrocad`)
crates/ferrocad_gen/ generated PyO3 skeleton bindings (module `ferrocad_gen`)
src/lib.rs GENERATED by tools/codegen.py (committed)
crates/ferrocad_geom/ geometry seam: Shape, History, GeometryBackend trait
crates/ferrocad_occt/ OCCT backend (needs OCCT 7.8+)
crates/ferrocad_part/ Part workbench: Part::Feature, Part::PropertyPartShape
crates/ferrocad_part_py/ the `Part` Python module (makeBox, Shape); links OCCT
crates/ferrocad/ the base app binary (cargo run -p ferrocad)
src/main.rs entry: calls ferrocad::run()
src/lib.rs run/run_as + register_core_module (edition entry point)
src/payload.rs finds/unpacks the embedded Python payload (cargo install)
build.rs embeds python/ + mods/ when staged (embedded-payload feature)
crates/ferrocad_parametric/ edition binary: base app + Part (cargo run -p ferrocad_parametric)
src/main.rs registers the Part module, then ferrocad::run_as
crates/ferrocad_viewport/ the bite-gpui viewport: InventorViewport (image path), NaviCube, sample scene
src/lib.rs the crate surface (InventorViewport, NaviCube)
src/inventor_view.rs the gpui element + pointer→pick→listener interaction
src/navicube.rs the navigation-cube controller (placement, sync, hit-test, snap)
src/scene.rs the sample Inventor scene + hover listener
crates/ferrocad_gpui/ library: FerroCAD's bite-gpui app shell (boots CPython)
src/lib.rs run/run_with + HostConfig + window options
src/python.rs embedded-CPython bridge (boot, model_tree, properties, evaluate)
src/shell.rs the Shell view: inspector / property editor / console / status bar
src/spike.rs kept feasibility spike (Python-declared UI, headless)
crates/ferrocad_widgets/ reusable bite-gpui widgets (window chrome, field, console)
crates/xtask/ cargo xtask tasks (bundle: stage a distribution)
src/main.rs `cargo xtask bundle [--debug]`
.cargo/config.toml the `cargo xtask` alias
mods/ workbenches (FreeCAD Mod/ equivalent); packaged into the dist
packaging/ per-platform packagers wrapping the staged payload
linux/appimage.sh AppImage (needs appimagetool)
macos/app.sh .app + .dmg
windows/portable.ps1 portable folder + .zip
icon/ferrocad.svg icon source
python/ferrocad_shell/ Python side of the shell (sample document, console, model queries)
tools/inventory.py M3a: parse upstream .pyi stubs into an API model (Python ast)
tools/test_inventory.py M3a tests (hermetic fixtures + upstream integration guard)
tools/codegen.py M3c: emit PyO3 skeleton bindings from the API model
tools/test_codegen.py M3c tests (hermetic generator logic)
tools/conformance.py M3d: run upstream Mod/Test files against our FreeCAD
tools/test_conformance.py M3d tests (hermetic harness helpers)
tests/test_parity.py behavioural checks (M0/M1)
tests/test_base_surface.py end-to-end FreeCAD surface smoke tests (M4)
tests/test_fc_core.py ferrocad_core via Python (M3b)
tests/test_codegen.py generated skeleton surface (M3c)