ferrocad_theme 0.1.4

FerroCAD's visual-language tokens: linear at the draw contract, packed sRGB at the gpui boundary
Documentation
  • Coverage
  • 100%
    61 out of 61 items documented0 out of 2 items with examples
  • Size
  • Source code size: 43.7 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 311.3 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 4s Average build duration of successful builds.
  • all releases: 3s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • ferrocad/ferrocad
    4 0 1
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • Vanuan

FerroCAD

The FerroCAD shell: 3D viewport, the Dev Tools command/model/property/console panes, and the modeless Grid settings card

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.4, twenty-one crates on crates.io, four self-contained artifacts (base and Parametric AppImage/zip pairs) 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:

chmod +x ferrocad-x86_64.AppImage && ./ferrocad-x86_64.AppImage

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:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
cargo install ferrocad
ferrocad

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 .app is ad-hoc signed, so a downloaded DMG needs xattr -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.py uses 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. Since ferrocad is just a document-object model (no "application"), the facade adds the small App-level layer it lacks: a document registry, ActiveDocument, unique-name allocation, and Version.
  • crates/ferrocad_py/ exposes the model as real #[pyclass] types via PyO3 (importable as ferrocad).
  • crates/ferrocad_core/ is the pure-Rust implementation behind ferrocad_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

./build.sh     # cargo build --release, then package the native libs into python/
./run.sh       # PYTHONPATH=python python3 hello_freecad.py
cargo run -p ferrocad   # the base app window (needs a display)
cargo run -p ferrocad_parametric -- --open target/demo/headless.FCStd  # basic .FCStd viewer
cargo run -p ferrocad_toy -- --render target/demo/toy.png          # headless PNG (no display, no OCCT)
cargo run -p ferrocad_parametric -- --render target/demo/part.png  # headless PNG with Part (needs OCCT)
cargo run -p ferrocad_parametric -- --script examples/headless_part.py \
    --render target/demo/part.png --size 1200x800             # box, bore, fillet
# full demo: headless script → .FCStd + PNG + STL
cargo run -p ferrocad_parametric -- --script examples/headless_gear.py \
    --render target/demo/gear.png --stl target/demo/gear.stl --size 1400x900
# open the saved file in the viewer
cargo run -p ferrocad_parametric -- --open target/demo/gear.FCStd
# or reopen and render/export it without a window
cargo run -p ferrocad_parametric -- --open target/demo/gear.FCStd \
    --render target/demo/gear_reopened.png --stl target/demo/gear_from_file.stl
cargo install ferrocad  # a self-contained binary: payload embedded, unpacked on first run
cargo xtask mods               # fetch upstream workbench scripts (default: Draft) into mods/
cargo xtask bundle             # stage the payload into target/dist/ferrocad (--debug for speed)
packaging/linux/appimage.sh    # wrap it into an AppImage (needs appimagetool)

--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.4
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:

. ../.toolchain/env.sh && cargo test --manifest-path Cargo.toml   # ferrocad_core unit tests
PYTHONPATH=python python3 -m unittest discover -s tests -v        # facade parity (M0/M1) + M4
PYTHONPATH=python python3 tests/test_fc_core.py                    # ferrocad bindings (M3b)
PYTHONPATH=python python3 tests/test_codegen.py                    # generated skeleton surface (M3c)
python3 tools/test_inventory.py                                    # .pyi parser (M3a)
python3 tools/test_codegen.py                                      # codegen logic (M3c)
python3 tools/test_conformance.py                                  # harness helpers (M3d)
PYTHONPATH=python python3 examples/file_roundtrip.py               # save/open a document (file load)
PYTHONPATH=python python3 examples/mvp_workflow.py                # the MVP acceptance demo (docs/direction/mvp-path.md)
. ../.toolchain/env.sh && cargo test -p ferrocad_gpui              # shell library tests (4)

Regenerate the M3c skeleton (crates/ferrocad_gen/src/lib.rs, committed):

python3 tools/codegen.py --root ../freecad-upstream --out crates/ferrocad_gen/src/lib.rs

Run upstream tests against our FreeCAD (conformance harness, M3d):

python3 tools/conformance.py --root ../freecad-upstream            # default curated files
python3 tools/conformance.py --root ../freecad-upstream --list     # list candidates

Documentation

The Rust crates carry Diátaxis-structured rustdoc — a tutorial, how-to guides, an explanation of the design, and the generated item reference:

cargo doc --no-deps --open                    # ferrocad_core + ferrocad_py
cargo doc --no-deps -p ferrocad_core          # one crate
cargo test --doc -p ferrocad_core             # the tutorial is a doc test

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 python3 examples/file_roundtrip.py

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/Unit system: 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-inches N'(expr)"), arithmetic, Value/UserString/getValueAs/toNumber.
  • Property/PropertyContainer, StringHasher/StringID.
  • Document/DocumentObject with default naming, a petgraph dependency graph, topological recompute order, and removeObject.
  • transactions (open/commit/abort + undo/redo), an Observer trait, and an expression parser/evaluator with Object.Property names and recompute().

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::Box etc. produce no shape),
  • upstream's .FCStd (zip) format — persistence is FerroCAD's own JSON,
  • App/Gui split, Coin3D, Qt,
  • FeaturePython scripting 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)