Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

User documentation

Generated documentation for the workflows FerroCAD implements today.

Each page is produced by running an ordinary Python script against the real FreeCAD facade: the code blocks are the code that executed and the tables are the values it read back. A workflow that is not implemented has no script, so it has no page — this index is the progress list, not a backlog.

The pages are grouped the way Diátaxis groups them: a tutorial teaches by doing, a how-to guide answers one task, the reference lists what exists, and an explanation page says why. Reference is the generated Python API reference.

Tutorials

Learning-oriented: follow one pass end to end.

PageWhat it showsRequires
Your first modelA single pass from an empty interpreter to a saved, parametric document: a document, a parameter, a second object that depends on it, a recompute, and the save that keeps it.the FreeCAD facade; no workbench module and no GPU

How-to guides

Task-oriented: do one specific thing.

PageWhat it showsRequires
Create a document and edit propertiesOpen a document, add a typed length and an enumeration, write a value through each, and see the error an invalid enumeration value raises.the FreeCAD facade; no workbench module and no GPU
Wire two objects together with an expressionAdd an expression that reads one object’s length from another, recompute, then change the input and watch the dependent value follow.the FreeCAD facade; no workbench module and no GPU
Undo an edit and reload the documentGroup an edit into a named transaction, undo and redo it, then save the document, close it, open it again, and read the values back.the FreeCAD facade; a writable temporary directory
Create, display and manipulate an objectThe toy workbench is the kernel-free reference edition. It shows the four things a workbench does: create an object, give it a visual representation, manipulate it through the generic interaction path, and record the edit as a transaction.the ferrocad_toy app script (it registers on import); no GPU: the mesh is read before it would be drawn
Drive a command from the selectionSelection is UI state the shell pushes into the facade, and a command reads it when the shell asks whether it is enabled. This page runs a command with no selection, selects an object, opens a dialog and accepts it, and deletes the selection.the ferrocad_toy app script (it registers on import); no window: the shell’s pushes are ordinary function calls
Turn the reference grid on and offThe Grid command is checkable and presentation-only. This page runs it with no window, shows the view request it returns, and reads the one switch every grid backend agrees on.the FerroCAD facade and ferrocad_shell; no GPU and no window

Reference

Generated from the live modules by tools/api_docs.py:

Explanation

Understanding-oriented: why it works this way.

  • How a click becomes a document edit — This page explains the shape of the system from the outside in. It is the answer to a question the how-to guides skip: why does the code look the way it does?

7 workflows.

Regenerating

python3 tools/userdocs.py            # refresh every page
python3 tools/userdocs.py --check    # non-zero exit if a page is stale
python3 tools/build_book.py          # assemble the mdBook edition

The check is the part that matters: it is cheap, needs no GPU, and it is what keeps the pages honest in review and in CI. The mdBook edition is assembled from these pages, so it inherits the same guarantee.

What these pages are not

  • Not rendered screenshots. A page shows the mesh the viewport would draw, summarised, but no pixels. Capturing a frame needs a GPU and an offscreen render path the shell does not expose yet; the image hook in tools/workflows/_capture.py is where that will attach.
  • Not upstream parity. Only what runs here is documented here; the gap is measured by docs/workbenches/conformance.md.

Your first model

A single pass from an empty interpreter to a saved, parametric document: a document, a parameter, a second object that depends on it, a recompute, and the save that keeps it.

Generated page. tools/userdocs.py writes it from tools/workflows/00_first_model.py. Edit the workflow, not this page; python3 tools/userdocs.py --check fails when the two disagree.

Requires: the FreeCAD facade; no workbench module and no GPU.

1. Import the application

Everything starts with the FreeCAD module. In FerroCAD it is a Rust-backed implementation of the same module upstream FreeCAD ships, so the code in this tutorial is ordinary FreeCAD Python.

import FreeCAD
Read backValue
Version0.1.3
Backendferrocad

2. Make a document

A document is the file you are building. newDocument creates it, makes it active, and returns it. Nothing is on disk yet.

doc = FreeCAD.newDocument('FirstModel')
Read backValue
Active documentFirstModel

3. Add a parameter

Objects are created by type name. App::FeaturePython is the generic scripted object; properties are added afterwards, each with a group and a description so a property editor has something to show.

parameters = doc.addObject("App::FeaturePython", "Parameters")
parameters.addProperty(
    "App::PropertyLength", "Length", "Model", "The one number that drives it"
)
parameters.Length = "20 mm"
Read backValue
Parameters.Length20 mm

4. Make something depend on it

A second object can read the first one through an expression. The expression is stored in the document, which is what makes the model parametric: the value follows the input on every recompute.

result = doc.addObject("App::FeaturePython", "Result")
result.addProperty("App::PropertyLength", "Width", "Model", "Derived width")
result.setExpression("Width", "Parameters.Length * 3")
doc.recompute()
Read backValue
Result.Width60 mm
Result.State[“Up-to-date”]

5. Change the input

Editing the parameter marks the dependent object for recompute. One call updates everything in dependency order — you never tell it what to update.

parameters.Length = "35 mm"
doc.recompute()
Read backValue
Parameters.Length35 mm
Result.Width105 mm

6. Save it

saveAs writes the document, including the expression. The path below is a temporary one; in your own script choose a real location. Opening it again gives back the same objects and the same values.

import os, tempfile
path = os.path.join(tempfile.mkdtemp(prefix="ferrocad-tutorial-"), "first.ferrocad")
doc.saveAs(path)
FreeCAD.closeDocument("FirstModel")
reopened = FreeCAD.open(path)
Read backValue
Objects after reopening[“Parameters”, “Result”]
Width after reopening105 mm

7. Where to go next

That is the whole loop: create, parameterise, recompute, save. The how-to guides take each part further — properties and validation, undo and persistence in detail, and the workbench contracts that put objects on screen.

import shutil
FreeCAD.closeDocument(reopened.Name)
shutil.rmtree(os.path.dirname(path))
Read backValue
Open documents[]

Create a document and edit properties

Open a document, add a typed length and an enumeration, write a value through each, and see the error an invalid enumeration value raises.

Generated page. tools/userdocs.py writes it from tools/workflows/01_documents_and_properties.py. Edit the workflow, not this page; python3 tools/userdocs.py --check fails when the two disagree.

Requires: the FreeCAD facade; no workbench module and no GPU.

1. Create a document

newDocument registers the document with the application and makes it active. The registry lives in the Rust core, so the facade keeps no state of its own.

doc = FreeCAD.newDocument('Demo')
Read backValue
Active documentDemo
Open documents[“Demo”]

2. Add properties

A feature object is created by type name. Properties are added one at a time and carry a group and a description, which is what a property editor shows. App::PropertyLength is a quantity, so it accepts a unit string and formats a value back with a unit.

part = doc.addObject("App::FeaturePython", "Part")
part.addProperty("App::PropertyLength", "Length", "Base", "Overall length")
part.addProperty("App::PropertyEnumeration", "Finish", "Base", "Surface finish")
part.Finish = ["As machined", "Anodised"]
part.Length = "10 mm"
part.Finish = "Anodised"
Read backValue
part.Length10 mm
part.FinishAnodised
Property types[“Finish”, “Length”]

3. An invalid value is rejected

The enumeration is a closed set. Assigning a value outside it raises ValueError and leaves the property unchanged; a script can rely on that instead of validating the set itself.

try:
    part.Finish = "Bogus"
except ValueError as exc:
    rejected = str(exc)
Read backValue
Error“Bogus” is not part of the enumeration
part.Finish is unchangedAnodised

4. Close the document

Closing unregisters the document from the application.

FreeCAD.closeDocument('Demo')
Read backValue
Open documents[]

The values above are read from the live object, not from a fixture: this page is generated by running the script in tools/workflows/.

Wire two objects together with an expression

Add an expression that reads one object’s length from another, recompute, then change the input and watch the dependent value follow.

Generated page. tools/userdocs.py writes it from tools/workflows/02_parametric_recompute.py. Edit the workflow, not this page; python3 tools/userdocs.py --check fails when the two disagree.

Requires: the FreeCAD facade; no workbench module and no GPU.

1. Two objects, one dependency

Expressions are evaluated on recompute, not on assignment. The document records the dependency edge, so it can order the objects without the script doing so.

doc = FreeCAD.newDocument("Parametric")

source = doc.addObject("App::FeaturePython", "Source")
source.addProperty("App::PropertyLength", "Length", "Base", "Input length")
source.Length = "10 mm"

derived = doc.addObject("App::FeaturePython", "Derived")
derived.addProperty("App::PropertyLength", "Result", "Base", "Computed")
derived.setExpression("Result", "Source.Length * 2")
doc.recompute()
Read backValue
Source.Length10 mm
Derived.Result20 mm
Derived.State[“Up-to-date”]

2. Change the input

Assigning to the source does not touch Derived; the next recompute does, because the dependency is stored in the document.

source.Length = "15 mm"
doc.recompute()
Read backValue
Source.Length15 mm
Derived.Result30 mm

3. Read the expression back

The expression is part of the document, not of the Python object.

expr = derived.ExpressionEngine

ExpressionEngine

[
  [
    "Result",
    "Source.Length * 2"
  ]
]

4. Close the document

FreeCAD.closeDocument('Parametric')
Read backValue
Open documents[]

Undo an edit and reload the document

Group an edit into a named transaction, undo and redo it, then save the document, close it, open it again, and read the values back.

Generated page. tools/userdocs.py writes it from tools/workflows/03_undo_and_persistence.py. Edit the workflow, not this page; python3 tools/userdocs.py --check fails when the two disagree.

Requires: the FreeCAD facade; a writable temporary directory.

1. Build a document

import tempfile
doc = FreeCAD.newDocument("Undoable")

part = doc.addObject("App::FeaturePython", "Part")
part.addProperty("App::PropertyLength", "Length", "Base", "Overall length")
part.addProperty("App::PropertyEnumeration", "Finish", "Base", "Surface finish")
part.Finish = ["As machined", "Anodised"]
part.Length = "10 mm"
doc.recompute()
Read backValue
part.Length10 mm

2. One transaction, one undo

Everything between openTransaction and commitTransaction is one undoable step. A workbench wraps each command in exactly this, which is why undo never lands in the middle of a command.

doc.openTransaction("Resize")
part.Length = "25 mm"
part.Finish = "Anodised"
doc.commitTransaction()
doc.recompute()
Read backValue
part.Length after the edit25 mm
part.Finish after the editAnodised
doc.undo(); doc.recompute()
Read backValue
part.Length after undo10 mm
part.Finish after undoAs machined
doc.redo(); doc.recompute()
Read backValue
part.Length after redo25 mm

3. Save and reload

saveAs writes the document format; open reads it into a new document with the same names and property values. The path is a temporary file, so it is not shown.

import os
path = os.path.join(tempfile.mkdtemp(prefix="ferrocad-userdocs-"), "undoable.ferrocad")
doc.saveAs(path)
doc_name = doc.Name
FreeCAD.closeDocument(doc_name)
Read backValue
Open documents after close[]
reopened = FreeCAD.open(path)
reloaded = reopened.getObject("Part")
Read backValue
Reloaded namePart
Reloaded Length25 mm
Reloaded FinishAnodised

4. Clean up

import shutil
FreeCAD.closeDocument(reopened.Name)
shutil.rmtree(os.path.dirname(path))
Read backValue
Open documents[]

Create, display and manipulate an object

The toy workbench is the kernel-free reference edition. It shows the four things a workbench does: create an object, give it a visual representation, manipulate it through the generic interaction path, and record the edit as a transaction.

Generated page. tools/userdocs.py writes it from tools/workflows/04_toy_workbench.py. Edit the workflow, not this page; python3 tools/userdocs.py --check fails when the two disagree.

Requires: the ferrocad_toy app script (it registers on import); no GPU: the mesh is read before it would be drawn.

1. Boot the edition

bootstrap builds the same sample document the shell opens at startup, then adds one slab and exposes the Toy console name. The shell calls exactly this function.

import json
import ferrocad_shell
import ferrocad_toy

payload = json.loads(ferrocad_toy.bootstrap("Toy"))

bootstrap

{
  "backend": "ferrocad",
  "document": "Toy",
  "version": "0.1.3"
}

2. The document the shell inspects

model_tree is the shell’s inspector feed: documents, objects and their type names. It goes through the same public API a workbench uses.

tree = json.loads(ferrocad_shell.model_tree())

model_tree

[
  {
    "label": "Toy",
    "name": "Toy",
    "objects": [
      {
        "label": "Params",
        "name": "Params",
        "type": "App::FeaturePython"
      },
      {
        "label": "Derived",
        "name": "Derived",
        "type": "App::FeaturePython"
      },
      {
        "label": "Group",
        "name": "Group",
        "type": "App::DocumentObjectGroup"
      },
      {
        "label": "Slab",
        "name": "Slab",
        "type": "App::FeaturePython"
      }
    ]
  }
]

3. The visual representation

The display provider turns the object’s Width, Depth, Height and Placement into a mesh, and carries Color and DisplayMode alongside it. The shell asks for these meshes; it never names the workbench.

meshes = json.loads(ferrocad_shell.stage_meshes())
slab_mesh = meshes[0]
positions = slab_mesh["positions"]
summary = {
    "object": slab_mesh["object"],
    "display": slab_mesh["display"],
    "color": slab_mesh["color"],
    "positions (vertices)": len(positions),
    "triangles": len(slab_mesh["triangles"]) // 3,
    "edges": len(slab_mesh["edges"]) // 2,
    "bounds": [
        [min(p[i] for p in positions) for i in range(3)],
        [max(p[i] for p in positions) for i in range(3)],
    ],
}

stage_meshes[0], summarised

{
  "bounds": [
    [
      -20.0,
      -20.0,
      0.0
    ],
    [
      20.0,
      20.0,
      20.0
    ]
  ],
  "color": [
    0.26,
    0.55,
    0.85,
    1.0
  ],
  "display": "shaded_edges",
  "edges": 24,
  "object": "Slab",
  "positions (vertices)": 24,
  "triangles": 12
}

4. Create an object

makeSlab is the workbench’s creation command. It adds the object with its properties and opens one named transaction, so undo treats it as a single step.

slab = ferrocad_toy.makeSlab(width=60, height=30, x=10, name="Wide")
Read backValue
NameWide
Width60 mm
Height30 mm
Placement.BaseVector (10, 0, 0)
DisplayModeShadedWithEdges

5. Manipulate it through the interaction path

A manipulator in the viewport resolves to a request, and the workbench supplies the edit. The toy_height grip writes Height; the generic transform gizmo composes a world-space matrix with the placement. Both arrive here as one transaction, so both are undoable.

response = json.loads(
    ferrocad_shell.interaction(
        "Wide", "toy_height", "set", "", json.dumps([42.0])
    )
)
Read backValue
Response{“ok”: true}
Height after the grip42 mm

The transform gizmo sends a row-major 4×4 matrix; the translation lives in the last column (m03, m13, m23).

move = [1, 0, 0, 25, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
response = json.loads(
    ferrocad_shell.interaction(
        "Wide", "transform", "apply", "", json.dumps(move)
    )
)
Read backValue
Response{“ok”: true}
Placement.Base after the moveVector (35, 0, 0)

6. Undo the manipulation

Because each interaction is a transaction, the document’s own undo stack reverses them one at a time. No workbench-side history is needed.

slab.Document.undo()
slab.Document.recompute()
Read backValue
Placement.Base after one undo (the move)Vector (10, 0, 0)
Height after one undo42 mm
slab.Document.undo()
slab.Document.recompute()
Read backValue
Height after two undos30 mm

7. What the workbench registered

The commands in the list come from FreeCADGui, not from the shell. enabled is the command’s IsActive evaluated against the current selection, which is why the edit commands are disabled right now.

commands = json.loads(ferrocad_shell.commands())
CommandGroupKindEnabled
App_DeleteEditForEditfalse
App_MoveEditForEditfalse
Mods_SettingsEditForEdittrue
Toy_MakeSlabToyForEdittrue
Toy_PolylineToyForViewtrue

The shell never links a workbench. Everything on this page reaches the document through FreeCAD, FreeCADGui or the module hook the edition registered at import time.

8. Close the document

FreeCAD.closeDocument('Toy')
Read backValue
Open documents[]

Drive a command from the selection

Selection is UI state the shell pushes into the facade, and a command reads it when the shell asks whether it is enabled. This page runs a command with no selection, selects an object, opens a dialog and accepts it, and deletes the selection.

Generated page. tools/userdocs.py writes it from tools/workflows/05_selection_and_commands.py. Edit the workflow, not this page; python3 tools/userdocs.py --check fails when the two disagree.

Requires: the ferrocad_toy app script (it registers on import); no window: the shell’s pushes are ordinary function calls.

1. Boot the edition

import json
import ferrocad_shell
import ferrocad_toy

ferrocad_toy.bootstrap("Toy")
Read backValue
Objects[“Params”, “Derived”, “Group”, “Slab”]

2. Enablement follows the selection

The shell pushes its selection into FreeCADGui.Selection; a command answers IsActive against it. Nothing is enabled or disabled by hand.

ferrocad_shell.set_selection('[]')
def delete_state():
    for command in json.loads(ferrocad_shell.commands()):
        if command["id"] == "App_Delete":
            return command["enabled"]
Read backValue
App_Delete, nothing selectedfalse
ferrocad_shell.set_selection('[{"object": "Slab"}]')
Read backValue
Selection[“Slab”]
App_Delete, one object selectedtrue

3. A command that asks for input

Toy_MakeSlab does not act immediately. It returns a dialog description and a token; the shell renders the fields natively and sends the typed values back to that token.

request = json.loads(ferrocad_shell.run_command("Toy_MakeSlab"))

Dialog request

{
  "accept": "Create",
  "fields": [
    {
      "kind": "quantity",
      "label": "Width",
      "min": 0.1,
      "name": "Width",
      "value": 40.0
    },
    {
      "kind": "quantity",
      "label": "Depth",
      "min": 0.1,
      "name": "Depth",
      "value": 40.0
    },
    {
      "kind": "quantity",
      "label": "Height",
      "min": 0.1,
      "name": "Height",
      "value": 20.0
    }
  ],
  "title": "Create Slab"
}

Accepting runs the command’s callback, which owns its transaction.

accepted = json.loads(
    ferrocad_shell.dialog_accept(
        json.dumps(
            {
                "token": request["token"],
                "values": {"Width": 50.0, "Depth": 25.0, "Height": 12.0},
            }
        )
    )
)
Read backValue
Accept response{“ok”: true, “status”: “created Slab001”}
Objects[“Params”, “Derived”, “Group”, “Slab”, “Slab001”]

4. Cancelling instead

A token is single-use. Cancelling drops it without running the callback, so a cancelled dialog leaves no transaction behind.

cancelled = json.loads(ferrocad_shell.run_command("Toy_MakeSlab"))
cancel_response = json.loads(
    ferrocad_shell.dialog_cancel(cancelled["token"])
)
Read backValue
Cancel response{“ok”: true}
Objects[“Params”, “Derived”, “Group”, “Slab”, “Slab001”]

5. Act on the selection

App_Delete is the shell’s own edit command. It reads the same selection snapshot and deletes what it names.

ferrocad_shell.set_selection('[{"object": "Slab"}]')
deleted = json.loads(ferrocad_shell.run_command("App_Delete"))
Read backValue
Delete response{“ok”: true, “status”: “deleted 1 object(s)”}
Objects[“Params”, “Derived”, “Group”, “Slab001”]

Every call above is a plain function on the shell’s Python module. The same functions are what the window calls while you click, which is why these pages run with no window at all.

6. Close the document

FreeCAD.closeDocument('Toy')
Read backValue
Open documents[]

Turn the reference grid on and off

The Grid command is checkable and presentation-only. This page runs it with no window, shows the view request it returns, and reads the one switch every grid backend agrees on.

Generated page. tools/userdocs.py writes it from tools/workflows/06_grid_visibility.py. Edit the workflow, not this page; python3 tools/userdocs.py --check fails when the two disagree.

Requires: the FerroCAD facade and ferrocad_shell; no GPU and no window.

1. Start from the default

FerroCAD.Grid is the semantic settings object for the reference grid. enabled is the switch a provider reads; it defaults to off.

import json
import ferrocad_shell

FerroCAD.Grid.reset()
Read backValue
FerroCAD.Grid.enabledfalse

2. Run the command

App_ToggleGrid is registered by the shell and is checkable. It flips FerroCAD.Grid.enabled and returns a view request; the shell applies that request and mirrors the result into FerroCADGui.ViewState.

first = json.loads(ferrocad_shell.run_command("App_ToggleGrid"))
Read backValue
Response{“ok”: true, “view”: {“grid”: true, “grid_alpha”: 1.0, “grid_color”: “#94a3b8”, “grid_major”: 100.0, “grid_minor”: 10.0, “grid_radius”: 0.0}}
FerroCAD.Grid.enabledtrue

3. Push the state the window would push

With no window, nothing has mirrored the request yet, so the check mark still reads the old state. set_view_state is the call the shell makes after it applies a request.

ferrocad_shell.set_view_state(json.dumps(first["view"]))
checked = {
    c["id"]: c["checked"]
    for c in json.loads(ferrocad_shell.commands())
    if c["id"] == "App_ToggleGrid"
}
Read backValue
App_ToggleGrid checked{“App_ToggleGrid”: true}
FerroCADGui.ViewState.gridtrue

4. Turn it off again

second = json.loads(ferrocad_shell.run_command("App_ToggleGrid"))
ferrocad_shell.set_view_state(json.dumps(second["view"]))
Read backValue
Response{“ok”: true, “view”: {“grid”: false, “grid_alpha”: 1.0, “grid_color”: “#94a3b8”, “grid_major”: 100.0, “grid_minor”: 10.0, “grid_radius”: 0.0}}
FerroCAD.Grid.enabledfalse

Every grid backend reads this one switch. A provider that kept its own copy would keep drawing after the toggle, which is exactly the defect docs/tasks/grid-visibility.md fixed.

Python API reference

Generated from the live modules by tools/api_docs.py. Each page is a signature and summary reference for what FerroCAD ships, not a tutorial: the workflows in ../user/ show the same surface being used.

The product surface is the FreeCAD/FreeCADGui pair, which deliberately mirrors upstream, so the authoritative description of behaviour remains FreeCAD’s own documentation. FerroCAD/FerroCADGui hold the FerroCAD-only extensions and are documented here alone. What this section adds is the exact shape of the surface here.

ModulePublic namesWhat it is
FreeCAD15Upstream-compatible surface: documents, objects, the base value types.
FreeCAD.Base13The Base value types FreeCAD exposes as a submodule.
FreeCAD.Units9Quantity parsing, units and number formatting.
FreeCAD.Console5The console sink a workbench prints through.
FreeCADGui20Upstream-compatible GUI surface: commands, selection and dialogs.
FerroCAD11FerroCAD-only surface: the semantic grid and the module hooks.
FerroCADGui15FerroCAD-only GUI surface: providers, settings, overlays and view state.
ferrocad_shell26The host bridge the shell calls. Not upstream API: it is how FerroCAD’s window (and tools/userdocs.py) drive the facade.

Scope and honesty

  • Generated, not curated. A page lists what the module actually exports when the generator runs. A missing name means missing here, not undocumented.
  • Signatures where Python exposes them. Names implemented by the PyO3 extension may not report a signature; those show as (...).
  • Not a parity claim. Upstream has far more surface. The measured gap lives in docs/workbenches/conformance.md.

Regenerating

python3 tools/api_docs.py            # refresh every page
python3 tools/api_docs.py --check    # non-zero exit if a page is stale

FerroCAD

FerroCAD-only surface: the semantic grid and the module hooks.

Generated page. tools/api_docs.py reads this surface from the module itself. Do not edit it by hand.

The FerroCAD-only Python API (the extensions upstream FreeCAD does not have).

A mod that imports this depends on FerroCAD; it is not upstream API.

Functions

FunctionSummary
display_mesh(obj)The display mesh for obj, from the first module that claims it.
interaction(obj, manipulator, action, element='', values=None)Apply a manipulator commit through the first module that supports it.
manipulator_specs(obj, element='')The declarative handles every module declares for obj, in order.
parameter_file()The parameter file in use, or None while the store is memory-only.
register_display_provider(provider)Let provider(obj) supply the display mesh of a document object.
register_interaction_provider(provider)Let provider(obj, manipulator, action, element, values) apply an edit.
register_manipulator_provider(provider)Let provider(obj, element) declare handles for a selection.
set_parameter_file(path)Point FreeCAD.ParamGet at path (always enabled).

Object Axis

The FerroCAD.Axis singleton (see the module docstring).

MemberSummary
apply() -> dictThe {"view": …} request for the current settings.
color_x (property)The X stem’s ink as a canonical lowercase #rrggbb.
color_y (property)The Y stem’s ink as a canonical lowercase #rrggbb.
color_z (property)The Z stem’s ink as a canonical lowercase #rrggbb.
enabled (property)Whether the world axis cross is shown.
ghost_alpha (property)The alpha of the occluded (ghosted) stems in 0..1.
labels (property)Whether the X/Y/Z labels are drawn.
length (property)The stem length in pixels.
opacity (property)The stems’ opacity in 0..1.
origin (property)Whether the origin node is drawn at the cross’s centre.
reset() -> NoneClear the axis store (only the axis provider’s own group).
schema() -> listA copy of :data:SETTINGS_SCHEMA (the rows the provider advertises).
snapshot() -> dictThe stored axis policy, in the same shape as the axis view.
width (property)The stem width in pixels.

Object Gizmo

The FerroCAD.Gizmo singleton (see the module docstring).

MemberSummary
active_color (property)The hovered/active handle’s ink as a canonical lowercase #rrggbb.
apply() -> dictThe {"view": …} request for the current settings.
enabled (property)Whether the manipulator gizmos are shown.
ghost_alpha (property)The alpha of the occluded (ghosted) handles in 0..1.
inactive_color (property)The idle handle’s ink as a canonical lowercase #rrggbb.
opacity (property)The handles’ opacity in 0..1.
reset() -> NoneClear the gizmo store (only the gizmo provider’s own group).
ring_snap_deg (property)The rotation ring’s snap increment in degrees.
schema() -> listA copy of :data:SETTINGS_SCHEMA (the rows the provider advertises).
show_readout (property)Whether the drag readout is drawn next to the gizmo.
size (property)The handle size in pixels.
snapshot() -> dictThe stored gizmo policy, in the same shape as the gizmo view.
width (property)The handle stroke width in pixels.

Object Grid

The FerroCAD.Grid singleton (see the module docstring).

MemberSummary
apply() -> dictThe {"view": …} request for the current settings.
axis_cross (property)Deprecated: moved to :attr:FerroCAD.Axis.enabled.
color (property)The grid ink as a canonical lowercase #rrggbb.
enabled (property)Whether the world grid’s ground layer is shown.
major (property)The world grid’s major (line) step, in millimetres.
minor (property)The world grid’s minor (dot) step, in millimetres.
opacity (property)A multiplier (0..1) over the token alphas.
radius (property)The fixed world-grid extent (mm); 0 is camera-derived.
reset() -> NoneClear the grid store, including the pre-S4 legacy keys.
schema() -> listA copy of :data:SETTINGS_SCHEMA (the rows the provider advertises).
shadow_opacity (property)The requested shadow strength, 0..1 (nothing draws it yet).
shadows (property)Whether grid shadows are requested (nothing draws them yet).
snapshot() -> dictThe applied state when the shell mirrors it, else the stored policy.
spacing (property)The minor step (mm); setting it writes the next decade as the major step.

FerroCADGui

FerroCAD-only GUI surface: providers, settings, overlays and view state.

Generated page. tools/api_docs.py reads this surface from the module itself. Do not edit it by hand.

The FerroCAD-only Gui API (the extensions upstream FreeCADGui lacks).

ViewState is a read-only snapshot pushed by the shell.

Functions

FunctionSummary
activeProvider(feature)The active backend for feature, or None when none is registered.
addDocumentListener(observer) -> NoneRegister observer for document lifecycle notifications.
addOverlay(node) -> NoneRegister a Coin node as a viewport overlay (FerroCAD extension).
providerSkin(feature, backend=None)The WGSL material program of feature’s backend, or None.
providers(feature=None) -> listThe provider manifests, in registration order.
refreshProviders(feature: str) -> dictAsk the active backend of feature to re-read the shared state.
registerProvider(feature: str, backend: str, label: str, command=None, priority: int = 0, activate=None, deactivate=None, refresh=None, settings=None, skin=None)Declare that backend provides feature (called from InitGui.py).
removeDocumentListener(observer) -> boolUnregister observer; returns whether it was registered.
removeOverlay(node) -> boolDrop a previously registered overlay, keyed by Python object identity.
setActiveProvider(feature: str, backend: str) -> dictMake backend the active provider for feature and apply it.
setSetting(feature: str, name: str, value) -> dictCoerce, validate and store one setting, then refresh the provider.
settings(feature: str, backend=None) -> listThe settings rows of feature’s active provider (or backend).
unregisterProvider(feature: str, backend: str) -> boolDrop a provider record; returns whether it existed.

Object ViewState

The shell’s presentation state, pushed by the shell (read-only here).

MemberSummary
axis_cross (attribute)—
grid (attribute)—
grid_major (attribute)—
grid_minor (attribute)—
grid_radius (attribute)—
projection (attribute)—

Values

NameSummary / value
PROVIDER_PREFERENCES‘User parameter:BaseApp/Preferences/Mods’

FreeCAD.Base

The Base value types FreeCAD exposes as a submodule.

Generated page. tools/api_docs.py reads this surface from the module itself. Do not edit it by hand.

FreeCAD.Base — the core data types.

Class BoundBox

MemberSummary
Center (attribute)—
XLength (attribute)—
XMax (attribute)—
XMin (attribute)—
YLength (attribute)—
YMax (attribute)—
YMin (attribute)—
ZLength (attribute)—
ZMax (attribute)—
ZMin (attribute)—
add(x, y, z)—
enlarge(amount)Grow the box by amount in every direction (BoundBox.enlarge).
getIntersectionPoint(point, direction)The first box-surface point hit by a ray from point along direction.
intersect(other)—
intersected(other)—
isInside(value)—
isValid()—
setVoid()—

Class CADKernelError

Kernel failure (Part.OCCError when the Part module is absent).

Class Material

MemberSummary
DiffuseColor (attribute)—

Class Matrix

MemberSummary
A (attribute)—
col(col)—
decompose()(shear, scale, rotation, move) such that self == move * rotation * scale * shear.
determinant()—
diagonal()—
hasScale(tol=0.0)Classify scaling; returns a FreeCAD.ScaleType member like upstream.
inverse()—
isNull()—
isUnity(tol=0.0)—
move(*args)—
multVec(v)Transform a vector by this matrix (FreeCAD Matrix.multVec).
multiply(o)—
nullify()—
rotateX(angle)—
rotateY(angle)—
rotateZ(angle)—
row(row)—
scale(*args)—
setCol(col, v)—
setRow(row, v)—
transpose()—
unity()—

Class Placement

MemberSummary
Base (attribute)—
Matrix (attribute)—
Rotation (attribute)—
copy()A detached copy (Placement.copy).
inverse()—
isSame(o, tol=0.0)—
multVec(v)Transform a vector by this placement (FreeCAD Placement.multVec).
rotate(center, axis, angle, comp=False)Rotate this placement in place (Placement.rotate).
toMatrix()—
translate(*args)Move this placement in place (Placement.translate).

Class Precision

Base::Precision — the global tolerance defaults OCCT uses.

MemberSummary
angular() -> float—
approximation() -> float—
confusion() -> float—
firstParameter() -> float—
intersection() -> float—
lastParameter() -> float—
squareConfusion() -> float—

Class Quantity

MemberSummary
Format (attribute)—
Unit (attribute)—
UserString (attribute)—
Value (attribute)—
getValueAs(unit)—
toStr()—
value_in(unit)—
value_mm()—

Class Rotation

MemberSummary
Angle (attribute)—
Axes (attribute)—
Axis (attribute)—
Matrix (attribute)—
Pitch (attribute)—
Q (attribute)—
RawAxis (attribute)—
Roll (attribute)—
Yaw (attribute)—
getYawPitchRoll()—
inverse()—
invert()—
inverted()inverted(): a new rotation with the inverse value (the quaternion conjugate). Upstream’s Rotation::inverse() also negates the retained axis, so an axis set for an angle-0 rotation survives the inversion.
isSame(o, tol=0.0)—
multVec(v)Rotate a vector (FreeCAD Rotation.multVec).
multiply(o)—
setYawPitchRoll(yaw, pitch, roll)—
toMatrix()—

Class ScaleType

Scaling mode returned by Matrix.hasScale() (Base::ScaleType).

MemberSummary
NoScaling (attribute)—
NonUniformLeft (attribute)—
NonUniformRight (attribute)—
Other (attribute)—
Uniform (attribute)—

Class TypeId

MemberSummary
Name (attribute)—
createInstance()—
fromName(name)—
getAllDerivedFrom(_parent)—

Class Unit

Class Vector

MemberSummary
Length (attribute)—
add(o)—
cross(o)—
distanceToLine(base, direction)distanceToLine(base, direction): distance from this point to the line.
distanceToPoint(o)—
dot(o)—
getAngle(o)—
isEqual(o, tol)—
multiply(factor)multiply(factor): scale in place (upstream mutates and returns self).
negative()—
normalize()normalize(): normalise in place and return self (upstream’s semantics).
projectToPlane(base, normal)projectToPlane(base, normal): project in place onto the plane through base with the given normal.
scale(x, y=None, z=None)scale(factor) or scale(x, y, z): scale in place (upstream mutates and returns self). Draft’s fillets use the three-argument form.
sub(o)—
x (attribute)—
y (attribute)—
z (attribute)—

Class Vector2d

MemberSummary
rotate(angle)—
x (attribute)—
y (attribute)—

FreeCAD.Console

The console sink a workbench prints through.

Generated page. tools/api_docs.py reads this surface from the module itself. Do not edit it by hand.

FreeCAD.Console — minimal logging facade.

Functions

FunctionSummary
PrintError(text: str = '', end: str = '\n') -> None—
PrintLog(text: str = '', end: str = '\n') -> None—
PrintMessage(text: str = '', end: str = '\n') -> None—
PrintStatus(*args, **kwargs) -> NoneCompatibility stub; upstream prints an optional status/type line.
PrintWarning(text: str = '', end: str = '\n') -> None—

FreeCAD.Units

Quantity parsing, units and number formatting.

Generated page. tools/api_docs.py reads this surface from the module itself. Do not edit it by hand.

FreeCAD.Units — the units facade.

Functions

FunctionSummary
listSchemas(*args)Return the full schema tuple, or one schema name by index.
parseQuantity(expression)Parse a unit expression into a :class:Quantity.
schemaTranslate(quantity, schema)Translate a quantity into another schema’s textual pieces.
toNumber(value, format='g', decimals=0)Format a number or Quantity as a string.
translateUnit(expression)Return the value of expression in internal units.

Class NumberFormat

Format codes accepted by toNumber.

MemberSummary
Fixed (attribute)—
General (attribute)—
Scientific (attribute)—

Class Quantity

MemberSummary
Format (attribute)—
Unit (attribute)—
UserString (attribute)—
Value (attribute)—
getValueAs(unit)—
toStr()—
value_in(unit)—
value_mm()—

Class Unit

Values

NameSummary / value
Radian57.29577951308232 deg

FreeCAD

Upstream-compatible surface: documents, objects, the base value types.

Generated page. tools/api_docs.py reads this surface from the module itself. Do not edit it by hand.

A drop-in, Rust-backed stand-in for FreeCAD’s Python App module.

Functions

FunctionSummary
ParamGet(path='', create=True)Return the parameter group rooted at path.
Version()—
activeDocument()—
closeDocument(name)—
getDocument(name)—
listDocuments()—
newDocument(name=None, hidden=False, temp=False)—
openDocument(path, hidden=False, temporary=False)—
writeRecoverySnapshotToTransientDir(doc, compressed=True)Write a recovery snapshot (metadata + document) to doc.TransientDir.

Class Document

MemberSummary
ActiveObject (attribute)—
Comment (attribute)—
CountObjects (attribute)—
FileName (attribute)—
Label (attribute)—
MemSize (attribute)—
Meta (attribute)—
Name (attribute)—
Objects (attribute)—
RedoCount (attribute)—
RedoNames (attribute)—
RootObjects (attribute)—
TopologicalSortedObjects (attribute)—
TransientDir (attribute)—
UndoCount (attribute)—
UndoMode (attribute)—
UndoNames (attribute)—
UndoRedoMemSize (attribute)—
abortTransaction()—
addObject(type, name=None, objProxy=None, viewProxy=None, attach=False, viewType=None)—
canWriteRecoverySnapshot()Recovery snapshots are only writable outside a transaction.
clearUndos()Drop the whole undo/redo history (FreeCAD clearUndos).
commitTransaction()—
copyObject(object, recursive=False, return_all=False)Copy one object (or a sequence of objects) from another document.
dumpContent(Compression=0)—
findObjects(Type=‘’, Name=‘’, Label=‘’)—
getAvailableRedos(id=0)The number of redo steps, or the depth of transaction id (FreeCAD getAvailableRedos).
getAvailableUndos(id=0)The number of undo steps, or the depth of transaction id (FreeCAD getAvailableUndos).
getBookedTransactionID()—
getObject(key)—
getTempFileName(basename)A file name inside the transient directory (getTempFileName).
isAutoCreated()—
load(path)—
openTransaction(name=‘’)—
recompute(objs=None, force=False, mode=False)Recompute the document (FreeCAD Document.recompute(objs, force, mode)).
redo()—
removeObject(name)—
restore()Re-load the document from its saved file (clearing current content).
restoreContent(data)Replace this document’s content from a dumpContent payload.
save()—
saveAs(path)Save under path, selecting the serializer from the suffix.
setAutoCreated(v)—
settings(namespace)Return a namespaced view over Meta (keys "<namespace>.<key>").
undo()—

Class DocumentObject

MemberSummary
Document (attribute)—
ExpressionEngine (attribute)—
ID (attribute)—
InList (attribute)—
Label (attribute)—
MustExecute (attribute)—
Name (attribute)—
OutList (attribute)—
PropertiesList (attribute)—
State (attribute)—
TypeId (attribute)—
ViewObject (attribute)—
addExtension(name)—
addObject(other)—
addProperty(type_id, name, group=‘’, doc=‘’, attr=0, read_only=False, hidden=False, locked=False, enum_vals=None)—
changeAttacherType(mode)changeAttacherType(name): provided by an attached extension (Part’s AttachExtensionPython), not by this crate.
configLinkProperty(*args, **kwargs)configLinkProperty(property, **links): provided by an attached extension (App’s LinkExtensionPython), not by this crate. Keyword arguments reach the provider as a final dict element of the tuple.
dumpContent()Serialize this object’s content to bytes (dumpContent).
dumpPropertyContent(name, Compression=0)Serialize one property to bytes (dumpPropertyContent).
enforceRecompute()—
evalExpression(source)Evaluate an expression source in this object’s context.
getDocumentationOfProperty(name)getDocumentationOfProperty: the property’s documentation string.
getElementIndex(name)getElementIndex(name): the index in an element reference.
getEnumerationsOfProperty(name)getEnumerationsOfProperty: an enumeration’s allowed values, else None.
getGroupOfProperty(name)getGroupOfProperty: the UI group the property belongs to.
getLinkedObject(resolve=True)getLinkedObject(resolve=True): the object a link points at.
getObject(name)—
getParent()getParent(): the first group (plain, part or body) this object belongs to.
getParentGeoFeatureGroup()The geo-feature group (App::Part, PartDesign::Body) that lists this object.
getParentGroup()The plain group (or group-extension object) that lists this object.
getPropertyByName(name)—
getPropertyStatus(name=‘’)getPropertyStatus(name=""): with no name, the supported status names.
getStatusString()A short status string: Invalid[: message], Touched or Valid.
getSubObject(subname, retType=0, matrix=None, transform=True, depth=0)Resolve sub-object(s) and return them per FreeCAD’s retType convention: 0 → the sub-shape (Part.Edge/Part.Face, …) when subname names an element, else the DocumentObject; 1 → DocumentObject; 2 → (obj, Matrix, None); 3 → Placement; 4 → Matrix.
getTypeIdOfProperty(name)—
getTypeOfProperty(name)getTypeOfProperty: the property’s status flags as text names.
group_members()Resolve this object’s Group link list into DocumentObjects.
hasExtension(name)—
hasObject(other)—
isDerivedFrom(type_id)FreeCAD DocumentObject.isDerivedFrom(typeId).
isLink()isLink(): true for App::Link and its registered containers, and for the Part::LinkArray* types.
purgeTouched()Mark the object as unchanged (FreeCAD purgeTouched).
recompute()Recompute this object (FreeCAD DocumentObject.recompute).
removeExtension(name)—
removeObject(other)GroupExtension.removeObject(obj): drop obj from this group’s Group link list (which also clears its derived parent) and return the objects that were actually removed.
removeProperty(name)—
restoreContent(data)Restore this object’s content from bytes (restoreContent).
restorePropertyContent(name, data)Restore one property from bytes (restorePropertyContent).
setEditorMode(prop, _modes)Record a property editor mode (ReadOnly, …); POC: fires the event only.
setExpression(prop, source=None)—
setLink(value, subname=None)App::Link.setLink(obj): point LinkedObject at obj (or clear it with None). subname narrows the link to one sub-element of the target.
setPropertyByName(name, value)—
setPropertyStatus(name, val)setPropertyStatus(name, val): add (or, with a leading -, clear) flags.
set_group(value)Assign the Group link list from a sequence of objects (or names).
supportedProperties()—
touch(prop=None)Mark the object for recompute (FreeCAD DocumentObject.touch).

Class ParameterGrp

An in-memory stand-in for Base.ParameterGrp (typed, nested groups).

MemberSummary
Clear()—
Export(path)—
GetBool(name, default=False)—
GetBools()Names with a bool value in this group (FreeCAD returns names).
GetFloat(name, default=0.0)—
GetFloats()Names with a float value in this group (FreeCAD returns names).
GetGroup(name, _store=None, _group=None)—
GetGroupName()—
GetGroups()The immediate sub-group names (FreeCAD GetGroups).
GetInt(name, default=0)—
GetInts()Names with an int value in this group (FreeCAD returns names).
GetString(name, default=‘’)—
GetStrings()Names with a string value in this group (FreeCAD returns names).
GetUnsigned(name, default=0)—
GetUnsigneds()Names with an unsigned value in this group (FreeCAD returns names).
HasGroup(name)—
Import(path)—
RemBool(name)—
RemFloat(name)—
RemGroup(name)Remove a child group, unless a live handle still references it.
RemInt(name)—
RemString(name)—
RemUnsigned(name)—
SetBool(name, value)—
SetFloat(name, value)—
SetInt(name, value)—
SetString(name, value)—
SetUnsigned(name, value)—

Class ScaleType

Scaling mode returned by Matrix.hasScale() (Base::ScaleType).

MemberSummary
NoScaling (attribute)—
NonUniformLeft (attribute)—
NonUniformRight (attribute)—
Other (attribute)—
Uniform (attribute)—

Values

NameSummary / value
ActiveDocumentNone
backend‘ferrocad’

FreeCADGui

Upstream-compatible GUI surface: commands, selection and dialogs.

Generated page. tools/api_docs.py reads this surface from the module itself. Do not edit it by hand.

The Gui module: the command registry and the document views.

Selection is a read-only snapshot pushed by the shell.

Functions

FunctionSummary
activeDocument()The GuiDocument for FreeCAD.ActiveDocument, or None.
addCommand(name: str, command) -> NoneRegister command under name ("<Workbench>_<Action>").
addDocumentObserver(observer) -> NoneUpstream-shaped alias for :func:FerroCADGui.addDocumentListener.
addModule(name: str) -> NoneRecord a module name for the next commit without importing it.
doCommand(text: str)Evaluate a Python command string in FreeCAD’s namespace.
getDocument(name)The GuiDocument for an open App document, or None.
getMainWindow()The headless main window (a stable singleton).
listCommands() -> listThe registered command ids, in registration order.
removeCommand(name: str) -> NoneDrop name from the registry (a no-op when absent).
removeDocumentObserver(observer) -> boolRemove a GUI document observer; returns whether it was registered.
runCommand(name: str, *args)Invoke name and return whatever Activated returned.

Class GuiDocument

The GUI side of one open App document.

MemberSummary
close() -> NoneForget the view and its scene graph; safe to call more than once.

Class InputHint

One status-bar hint: text plus any formatting args.

MemberSummary
args (attribute)—
text (attribute)—

Class UserInput

The small user-input vocabulary the task panels compare against.

MemberSummary
KeyDelete (attribute)—
KeyEnter (attribute)—
KeyEscape (attribute)—
MouseLeft (attribute)—
MouseMiddle (attribute)—
MouseMove (attribute)—
MouseRight (attribute)—

Class View3DInventor

A document’s 3D view, shaped like upstream’s View3DInventor.

MemberSummary
addEventCallback(name, callback) -> NoneRegister callback for the Coin event name.
fitAll()Accept a fit request (there is no camera in a headless view).
getPoint(callback=None, *args)Start a Snapper point session on this view.
getSceneGraph()The document’s scene root (None when Coin is unavailable).
redraw()Accept a redraw request (the shell repaints from the scene graph).
removeEventCallback(name, callback) -> boolDrop callback from name; returns whether it was registered.

Object HintManager

The headless hint channel (FreeCADGui.HintManager).

MemberSummary
hide()Clear the last hint from the status channel.
last (property)The last hint passed to :meth:show, or None.
last_hint (property)—
show(*hints)Show hints and mirror their text into FreeCADGui.draft.hint.

Object PySideUic

FreeCADGui.PySideUic: the headless .ui loader.

MemberSummary
loadUi(path_or_file)Load a form from a path, a :/ resource or a QFile-like object.

Object Selection

A snapshot of what the shell has selected.

MemberSummary
getSelection() -> listThe selected document objects (upstream Gui.Selection).
getSelectionEx() -> list[(object, [subelement names])] for the current selection.
isSelected(obj) -> boolWhether obj (or an object name) is in the selection.

Object Snapper

The module-level snapper singleton (FreeCADGui.Snapper).

MemberSummary
active_snaps()The enabled mode names.
addHoldPoint(point=None)Add point (or the last snapped point) to the hold-snap list.
angle (property)The grid rotation in degrees (the native unit is radians).
clearPointConstraintProvider(provider=None)Clear the provider, or only when it is provider.
constrain(point, basepoint=None, axis=None)Project point onto the constraint line through basepoint.
constrain_reset()Alias for :meth:unconstrain, kept next to :meth:reset.
context (property)The current SnapContext for the workplane and grid state.
cycleSnapObject(*_args, **_kwargs)Raise: cycling objects needs a screen-space cursor under the mouse.
disable(snap)—
enable(snap)—
getPoint(callback, *args, on_cancel=None, on_finish=None)Start a point request and return immediately.
get_snap_toolbar()The checkable snap-bar command ids, in toolbar order.
hide()Hide the workplane grid (the native half of upstream hide).
isEnabled(snap=None)Whether snapping is globally enabled, or whether snap is on.
mask (property)The active mode names (a live list, in activation order).
mode_enabled(mode)Whether mode is in mask (an unsupported mode is never on).
off()End the current point request without invoking its callback.
reset()Restore the defaults (used by tests and a future workbench reset).
restack()Raise: there is no Coin tracker to lower in a headless build.
setAngle(angle)Rotate the snap grid about the workplane normal.
setEnabled(enabled=True)Enable or disable snapping globally; returns the new state.
setGrid(enabled=True)Show or hide (and enable or disable) the workplane grid.
setMode(snap, enabled=True)Explicitly set snap on or off; returns the resulting state.
setPointConstraintProvider(provider)Set the task panel that constrains snapped points.
setSelectMode(enabled)Set the select-mode flag (upstream hides snapping while selecting).
setTrackers(update_grid=True)Refresh and return the current SnapContext.
setWorkplane(workplane)Adopt workplane (a ferrocad_stage.Workplane) and clear rotation.
set_enabled(enabled=True)Enable or disable snapping globally; returns the new state.
set_mode(snap, enabled=True)Explicitly set snap on or off; returns the resulting state.
set_workplane(workplane)Adopt workplane (a ferrocad_stage.Workplane) and clear rotation.
show()Show the workplane grid (the native half of upstream show).
snap(point, candidates=None)Snap a world point and return the resolved (x, y, z).
snap_context (property)—
supported_modes()The modes with a native implementation, in toolbar order.
toggle(snap, set_to=None)Toggle a mode in mask (or set it explicitly); returns its state.
toggleEnabled()Flip the global snap switch; returns the new state.
toggleGrid()Flip the grid flag; returns the new state.
toggle_enabled()Flip the global snap switch; returns the new state.
toggle_snap(snap, set_to=None)Toggle a mode in mask (or set it explicitly); returns its state.
unconstrain()Drop the base point and constraint axis.
unsupported_modes()The Draft-only modes that raise when enabled, in upstream order.
workplane (property)The effective workplane, with :meth:setAngle applied.

Object draftToolBar

FreeCADGui.draftToolBar: the headless Draft task-panel adapter.

MemberSummary
Activated()Mark the task panel active and make it the Snapper’s UI adapter.
Deactivated()End the task, finishing a source command that owns the panel.
escape()Cancel the current task: end snapping and finish the source command.
offUi()Release the panel’s command and provider state.
setAutoGroup(value=None)Set (or clear) the autogroup name; returns the stored value.
setStyleButton(*_args, **_kwargs)Raise: the Draft style button is a Qt widget FerroCAD does not build.
validatePoint(point=None)Apply the current constraint to point and return it.

ferrocad_shell

The host bridge the shell calls. Not upstream API: it is how FerroCAD’s window (and tools/userdocs.py) drive the facade.

Generated page. tools/api_docs.py reads this surface from the module itself. Do not edit it by hand.

App-shell helpers used by the FerroCAD shell library.

Every entry point returns a JSON string.

Functions

FunctionSummary
add_namespace(name: str, value) -> NoneExpose value to the console under name.
axis_summary() -> strThe axis’s provider and stem size for the shell’s popover.
bootstrap(doc_name: str = 'Shell') -> strCreate (or recreate) the sample document and report host status.
close_document(name: str) -> strClose a document by name.
commands() -> strThe registered workbench commands, with resources and live state, as JSON.
dialog_accept(payload_json: str) -> strAccept an open dialog: {"token": ..., "values": {...}}.
dialog_cancel(token: str) -> strCancel an open dialog and drop its token.
evaluate(code: str) -> strEvaluate one console line, capturing stdout/stderr and any traceback.
gpu_adapter() -> strThe persisted viewport GPU adapter setting (auto by default).
grid_summary() -> strThe grid’s provider and spacing for the shell’s popover.
hello() -> strA first console line, proving the interpreter is live.
interaction(object_name: str, manipulator: str, action: str, element: str, values_json: str) -> strApply a manipulator commit through the module that supports it.
load_workbenches(dirs_json: str = '[]') -> strBest-effort workbench loading.
manipulator_specs(payload_json: str) -> strThe handles the mods declare for the shell’s current selection, as JSON.
model_tree() -> strDocuments and their objects, for the inspector pane.
open_document(path: str) -> strOpen a saved document and make it active (the --open path).
properties(object_name: str) -> strProperty rows for one object, for the property editor pane.
provider_features() -> strEvery registered provider feature, with its backends and active flag.
provider_skin(payload_json: str) -> strOne feature’s provider-supplied WGSL source, for the shell to build.
run_command(name: str) -> strInvoke a workbench command by id; returns {ok, status?, dialog?, token?}.
set_gpu_adapter(payload_json: str) -> strPersist the viewport’s GPU adapter choice from the Dev Tools panel.
set_property(object_name: str, prop: str, text: str) -> strSet one property from its editor string, in a named transaction.
set_provider(payload_json: str) -> strSwitch one feature’s active provider from the shell’s popover.
set_selection(pairs_json: str) -> strReplace FreeCADGui.Selection with the shell’s selection.
set_view_state(state_json: str) -> strMirror the shell’s presentation state to FerroCADGui.ViewState.
stage_meshes() -> strThe tessellated shapes the viewport draws, as JSON.

How a click becomes a document edit

This page explains the shape of the system from the outside in. It is the answer to a question the how-to guides skip: why does the code look the way it does?

The facade is the contract, and it keeps no state

import FreeCAD is the product surface. Everything else — the shell, a workbench module, a test — is a client of it.

The Python package is a thin facade over a Rust core, and the facade deliberately owns nothing. Documents, objects, properties and undo all live in the core; the facade forwards. That is why you can close a document from one script and read it from another: there is no per-script model to fall out of sync.

The same property is what makes the workflow pages in this guide possible. They create documents, edit them and delete them without a window, because there is nothing window-shaped in the model.

The shell is a client, not a layer

The application shell does not link a workbench and does not reach into the document model directly. It calls a small set of Python functions (ferrocad_shell), each of which returns JSON:

The shell asks forThe functionWhat comes back
the document treemodel_tree()names, labels, type names
one object’s editorsproperties(name)rows for the property pane
what the viewport drawsstage_meshes()tessellated shapes
the actionscommands()ids, groups, enablement, check state
to run onerun_command(id)a status, or a dialog and a token

Every one of those is an ordinary call into FreeCAD or FreeCADGui. Nothing in ferrocad_shell names a workbench. That is a rule, not a style preference: it is what lets the same shell host the toy edition, a Part edition, or a future workbench without a rebuild. See docs/workbenches/actions-and-tools.md.

A click, step by step

  1. The viewport reports a pick. The shell turns it into a selection snapshot and pushes it into FreeCADGui.Selection.
  2. A command’s IsActive is evaluated against that snapshot, so the button enables or disables without anyone telling it to.
  3. Running the command returns one of a small number of things: a status, a {"view": …} request, a {"tool": …} request, or a dialog description plus a token.
  4. A dialog is data, not a callback. The shell renders the fields natively and sends the typed values back to the token. The command’s on_accept then opens the transaction.

That last point is the one people find surprising. Workbenches do not build Qt widgets in FerroCAD; they describe a form. The description is ordinary Python data, so it crosses the JSON boundary, and the window decides how it looks. This is also why a dialog can be tested with no window at all.

  1. The edit itself is one document transaction, so undo reverses the click, not a half-applied intermediate state.

Several providers, one feature

Where two modules can supply the same capability — a grid, for instance — they register as providers of a feature rather than competing silently. The registry picks one (priority, then a saved choice), and only the active backend draws. Mods_Settings switches them.

This is why the grid has more than one implementation and still one UI: the grid feature owns the command and the appearance settings, and the provider owns the drawing. docs/workbenches/mod-providers.md specifies the contract.

The consequence is a rule for anything that adds a provider: the switch that turns a feature on and off belongs to the feature, not to the provider. When two stores disagree about “on”, a toggle moves one of them and the other keeps drawing — the defect recorded in docs/tasks/grid-visibility.md.

What this buys

  • Workflows are testable without a GPU or a display. The model path is the same one a window uses; only the pixels differ.
  • The window is replaceable. A different shell can drive the same facade.
  • Documentation can be executed. The pages in this guide are generated by running the code they show, which is only possible because the model is reachable without the UI.

Where to read further

Mods: the external contract

Status: guide (2026-10-22).

This section is for people who write a FerroCAD mod, drive FreeCAD from a script, or drive the JSON bridge from an automated client. It describes the surface FerroCAD actually accepts today, with runnable snippets.

It is deliberately not:

Use this section for the contract and the patterns; use those for the internals and the exact signatures.

Who this is for

You areStart here
Writing a mod or workbenchminimal-mod.md, then mod-anatomy.md
Writing a script that drives FreeCADcommands.md and display-and-interaction.md
Writing an automated client (AI agent, MCP server)the JSON table below, then commands.md

What a mod is in FerroCAD

A mod is a directory under a mods path, containing Python scripts that run at boot:

mods/
  MyMod/
    Init.py        # non-GUI hook
    InitGui.py     # GUI hook (commands, feature providers, overlays)
    Resources/     # icons, shaders, data
    package.xml    # conventional FreeCAD metadata; not read by FerroCAD

Which FreeCAD?

A mod that imports only FreeCAD/FreeCADGui and uses only upstream names is FreeCAD-compatible: a port of an upstream workbench should need nothing else. Importing FerroCAD/FerroCADGui marks the mod FerroCAD-only — a deliberate dependency, and worth stating in the mod’s README. The boundary is docs/architecture/api-namespaces.md.

The loader does not use an upstream Workbench class, there is no build step, and there is no package manager. FreeCADGui.addWorkbench does not exist. The loader runs Init.py then InitGui.py for every subdirectory (see mod-anatomy.md). Registration is a side effect of running the script.

A mod is not sandboxed: it runs in the same interpreter as the application. The isolation rules in mod-anatomy.md say where a failure is contained and where it is not.

The reference mods in this tree are:

ModWhat it contributes
mods/GridOverlaythe GridOverlay_Settings command and the ferrocad backend of the grid feature
mods/GridOverlayCointhe GridOverlayCoin_Settings command, the coin backend, and a Coin overlay
python/ferrocad_toy/__init__.pya complete small mod in one file: two commands, a display provider, an interaction provider

The five things a mod can contribute

ContributionAPIRegistration lifetime
CommandsFreeCADGui.addCommand(name, command)until removeCommand(name); a duplicate id raises
A display providerFerroCAD.register_display_provider(provider)process lifetime; there is no unregister
An interaction providerFerroCAD.register_interaction_provider(provider)process lifetime; there is no unregister
Feature providersFerroCADGui.registerProvider(...)until unregisterProvider(feature, backend); re-registering replaces the record
OverlaysFerroCADGui.addOverlay(node) / removeOverlay(node)until removed; needs a pivy.coin facade that provides the bridge
  • A display provider is asked per document object: “give me a mesh, or None”.
  • An interaction provider is asked per manipulator commit: “apply this edit, or return None”.
  • A feature provider is a named backend for a named feature (for example the native grid versus the Coin grid). Exactly one backend per feature is active.
  • An overlay is a Coin node drawn beside the document scene, not a per-object display mesh.

Settings and parameters are not a sixth contribution: they are the persistence layer a mod uses (settings-and-parameters.md). A feature provider can advertise its knobs with settings=[...], which is what the shell renders under Settings/<Feature>; a provider without a schema keeps its command as the settings entry (providers.md). A feature provider may also implement refresh, which FerroCADGui.refreshProviders(feature) runs when the feature’s shared state changes without a switch (providers.md).

Reading order

  1. minimal-mod.md — a complete mod you can load today.
  2. mod-anatomy.md — directory, scripts, lifecycle, isolation.
  3. commands.md — commands, resources, dialogs, view/tool requests.
  4. display-and-interaction.md — meshes and edits.
  5. providers.md — one backend per feature, and who owns the switch.
  6. settings-and-parameters.md — ParamGet and FerroCAD.Grid.
  7. testing-a-mod.md — headless tests, no GPU.

The JSON boundary (for automated clients)

ferrocad_shell is the host bridge. Every function below returns a JSON string; all of them run with FreeCAD.GuiUp == 0.

FunctionReturns
bootstrap(doc_name="Shell")the sample document, version and backend
model_tree()documents and their objects
properties(object_name)property rows for the editor
set_property(object_name, prop, text){ok, error}; one transaction
evaluate(code){ok, output} from one console line
stage_meshes()the display meshes to draw
interaction(object_name, manipulator, action, element, values_json){ok, ...} from the interaction provider
commands()command ids, resources, surface, parent, enabled, checked
run_command(name){ok, status?, value?, view?, tool?, dialog?, token?}
dialog_accept(payload_json)the callback’s result; payload is {"token": ..., "values": {...}}
dialog_cancel(token){ok: true}
set_selection(pairs_json){ok: true}; payload is [{"object": name, "elements": [...]}, ...]
set_view_state(state_json){ok: true}; mirrors the shell’s view state

The generated signatures are in ../python-api/ferrocad_shell.md.

import json
import ferrocad_shell

for command in json.loads(ferrocad_shell.commands()):
    print(command["id"], command["enabled"])

FreeCADGui and FreeCAD are ordinary Python modules. An automated client that can call Python can use them directly; a client that can only exchange JSON should go through the functions above.

Background

What is not implemented

These are stated here so a client does not code against them:

  • FreeCADGui.addWorkbench, FreeCADGui.activeDocument, and ViewProvider are absent. A mod is the script contract above, not an upstream workbench object.
  • FreeCADGui.Selection is read-only. The shell pushes the snapshot; a mod cannot add or remove selection entries.
  • package.xml is not read by the FerroCAD loader.
  • Parameters are in memory (ParamGet is a shim); there is no cross-session persistence yet.
  • Overlays need a pivy.coin build that exposes addOverlay/removeOverlay. Drawing them needs the viewport and a GPU.

Commands

Status: guide (2026-10-22).

A command is a Python object registered with FreeCADGui. It is the entry point for a user action; it may edit the document, return a dialog description, or return a presentation request.

The object

Only Activated is required. The other three methods are optional.

import FreeCAD
import FreeCADGui
import FerroCADGui

class MakeThing:
    def GetResources(self):
        return {
            "MenuText": "Make Thing",
            "ToolTip": "Add a thing to the document",
            "Accel": "Ctrl+Shift+T",
            "Group": "MyMod",
            "CmdType": "ForEdit",
        }

    def IsActive(self):
        return FreeCAD.ActiveDocument is not None

    def IsChecked(self):          # only for a checkable command
        return FreeCAD.GuiUp and FerroCADGui.ViewState.grid

    def Activated(self):
        return {"status": "not implemented yet"}


FreeCADGui.addCommand("MyMod_MakeThing", MakeThing())

Register at import time from InitGui.py (mod-anatomy.md). The conventional id is "<Workbench>_<Action>".

APIBehaviour
FreeCADGui.addCommand(name, command)adds the command; a duplicate id raises ValueError; a missing callable Activated raises TypeError
FreeCADGui.removeCommand(name)drops the command; a no-op when absent
FreeCADGui.listCommands()the ids in registration order
FreeCADGui.runCommand(name, *args)calls Activated(*args) and returns the raw Python result; an unknown id raises KeyError

runCommand forwards arguments, as upstream does. The shell’s JSON path calls Activated() with no arguments.

GetResources

The shell reads these keys when it builds the viewport header and the action dock:

KeyUsed asDefault
MenuTextbutton labelthe command id
ToolTiphover text""
Accelshortcut string, for example "Ctrl+Shift+S"""
Groupordering label"" (registration order)
CmdTypemetadata: "ForEdit" or "ForView"""
Checkablethe row is a toggle: its popover stays open after the click so the status dot followsFalse
FerroCAD_Surfacewhere the action lives: grid, workplane, axis, snap, radial, mods, dock, hiddendock
FerroCAD_Parentthe surface’s primary command, for a row inside its popover""
FerroCAD_Shorta compact label for that popover row (Grid for Snap Grid)MenuText
FerroCAD_Tonea radial slot’s semantic class: structural, additive or destructivestructural
Pixmapaccepted and ignored; the controls are text today—

FerroCAD_Surface, FerroCAD_Parent, FerroCAD_Short and FerroCAD_Tone are FerroCAD extensions. Leave them out and the command lands on the dock, which is always safe; upstream FreeCAD ignores unknown resource keys. A command with FerroCAD_Surface: "grid" and no FerroCAD_Parent is the primary of that header control (its label runs it); a command with FerroCAD_Parent: "App_ToggleGrid" appears in the popover under it. hidden keeps a command out of the header and the dock; the Dev Tools command list and the app menu can still offer it. radial is special: the command is not a dock row at all — it becomes a wedge in the in-situ wheel, ordered by FerroCAD_Tone (structural, additive, destructive), so the destructive slot is always last (south).

A popover row keeps the menu open when it represents state: a command declares Checkable: True or defines IsChecked, and the generated provider rows are state rows too. Several filters — or a backend choice — can then be set in one visit and the status dots update in place. An action row (a settings jump, a reload) still dismisses the menu, and the app menu (a launcher, not a control popover) closes on its one row. The command decision uses the presence of the flag or hook, never IsChecked() == True, so an unchecked toggle and a non-toggle row are told apart.

CmdType is metadata only. The shell does not wrap Activated in a transaction because a command may return a dialog and act later; the command owns its transaction (docs/workbenches/actions-and-tools.md §3.3).

A raising GetResources is treated as {} and reported once. The command stays registered.

IsActive and IsChecked

Both take no arguments and are evaluated when the shell rebuilds the action list, not per frame.

MethodAbsentRaises
IsActivetreated as Truethe command is disabled and the error is reported once
IsCheckedtreated as Falsetreated as False and reported once

They read:

  • FreeCAD.ActiveDocument — may be None;
  • FreeCADGui.Selection — the shell’s read-only snapshot;
  • FerroCADGui.ViewState — the shell’s presentation mirror (grid, axis_cross, projection, grid_minor, grid_major, grid_radius).

A disabled command remains visible in the bar, so the user can see that it exists.

Activated return shapes

The shell calls Activated() through FreeCADGui._run(name), which never raises. The result is shaped as follows:

Activated returnsShell payload
None{"ok": true}
a non-dict value{"ok": true, "value": repr(value)}
{"status": "..."}{"ok": true, "status": "..."}
{"value": ...}{"ok": true, "value": ...}
{"view": {...}}{"ok": true, "view": {...}}
{"tool": "id"}{"ok": true, "tool": "id"}
a dialog spec (below){"ok": true, "dialog": {...}, "token": "dN"}
a raising Activated{"ok": false, "error": "Type: message"}

A dict may combine status, value, view and tool; the shell handles each. Keys it does not know are ignored until a later slice gives them meaning.

import json
import ferrocad_shell

result = json.loads(ferrocad_shell.run_command("App_ToggleGrid"))
print(result["view"]["grid"])

ferrocad_shell.run_command serialises FreeCADGui._run; FreeCADGui.runCommand returns the raw Python dict instead.

Dialogs

A dialog is data plus a token, not a callback on the wire. The command returns on_accept; the facade keeps the callable and sends the shell a description.

def apply(values):
    return {"status": "got %r" % (values,)}

class Ask:
    def GetResources(self):
        return {"MenuText": "Ask"}

    def Activated(self):
        return {
            "title": "Ask a number",
            "accept": "Apply",
            "fields": [
                {"name": "Count", "label": "Count", "kind": "int",
                 "value": 1, "min": 1, "max": 9},
            ],
            "on_accept": apply,
        }

The shell receives:

{
  "ok": true,
  "dialog": {
    "title": "Ask a number",
    "accept": "Apply",
    "fields": [
      {"name": "Count", "label": "Count", "kind": "int",
       "value": 1, "min": 1, "max": 9}
    ]
  },
  "token": "d1"
}

on_accept never crosses the JSON boundary. Only title, fields, accept and cancel are copied into dialog.

Field kinds

kindControlValue sent to the callbackValidation
texttext inputstrnon-empty when required
inttext inputintinteger; min/max
floattext inputfloatfloat; min/max
quantitytext inputfloat in canonical millimetresunit-aware parse; min/max
boolon/off chipboolnone
enumprevious/next choicestr (the selected option)the index is in range; values lists the options

The value key seeds the field. min/max apply to numeric kinds. The shell validates before accepting; an invalid value stays in the modal.

Accept and cancel

ActionShell bridgeFacade (private)
Accept with typed valuesferrocad_shell.dialog_accept('{"token": "d1", "values": {...}}')FreeCADGui._accept(token, values)
Cancelferrocad_shell.dialog_cancel("d1")FreeCADGui._cancel(token)
import json
import ferrocad_shell

run = json.loads(ferrocad_shell.run_command("View_GridSettings"))
accepted = json.loads(ferrocad_shell.dialog_accept(json.dumps({
    "token": run["token"],
    "values": {"enabled": True, "color": "#00ff00"},
})))
print(accepted["ok"], accepted["view"]["grid_color"])

Output:

True #00ff00

Lifecycle rules:

  • A token is single-use. Accept pops it before calling the callback; a second accept returns {"ok": false, "error": "no open dialog 'dN'"}.
  • Cancel drops the token without running the callback.
  • A callback that raises spends the token; the error comes back as {"ok": false, "error": "..."}.
  • The shell hosts one dialog at a time. In the app, a second request while one is open is rejected and its token is cancelled.
  • bootstrap() calls FreeCADGui._cancel_all(), so a dialog opened against the previous sample document is dropped when a new one is created.
  • The callback owns its transaction. An accepted dialog should be exactly one undo step.
  • A callback may return another dialog spec; the shell replaces the modal. That is the wizard mechanism.

View and tool requests

These are presentation-only and never open a transaction.

return {"view": {"grid": True}}
return {"view": {"axis_cross": False}}
return {"view": {"projection": "ortho"}}
return {"view": {"grid_minor": 1.0, "grid_major": 10.0, "grid_radius": 0.0}}
return {"view": {"grid_color": "#94a3b8", "grid_alpha": 0.5}}
return {"view": {"grid_overlay": True}}
return {"view": {"workplane": "activate"}}
return {"tool": "toy_polyline"}
KeyMeaning
gridshow or hide the reference grid
axis_crossshow or hide the world triad
projection"persp"/"perspective" or "ortho"/"orthographic"
grid_minor, grid_majorworld-grid steps in millimetres; both positive, major at least minor
grid_radiusfixed extent in millimetres; 0 is camera-derived
grid_color"#rgb" or "#rrggbb"
grid_alphamultiplier over the grid token alphas, 0..1
grid_overlayhide the native ground in favour of an authored overlay; registered overlays draw either way
workplanean action: "activate", "clear" or "view"

The shell applies a request, then mirrors the resulting state to FerroCADGui.ViewState, which is what makes a checkable command’s IsChecked close the loop. An invalid projection, a non-positive step, or a major below the minor is rejected and nothing is applied.

"tool" names a tool registered by the edition in Rust (Contributions::with_tool). Python cannot register a tool; it can only request one. An unknown id reports no such tool <id> and starts nothing.

grid_overlay is a shell flag, not a Python ViewState attribute: ferrocad_shell.set_view_state() does not set it. The Coin grid mod uses it to hide the native ground while its overlay is bound.

Transactions and errors

  • One user-visible edit is one transaction. Open it in Activated (or in the on_accept callback), and leave no transaction open on return.
  • On failure, abort the transaction and return {"status": ...}.
  • Activated errors through the shell bridge are contained: the app keeps running and the status line gets the error.
  • IsActive/IsChecked errors disable or uncheck one command only.

Next

Display and interaction providers

Status: guide (2026-10-22).

Two module hooks let a mod supply what the shell needs without the shell naming the mod:

HookRegistered withAsked when
displayFerroCAD.register_display_provider(provider)the shell syncs the document’s meshes
interactionFerroCAD.register_interaction_provider(provider)the user commits a manipulator edit

Both are registered once, at import time. Both use the same protocol: return None for a request you do not own, and the next provider is asked.

import FerroCAD

FerroCAD.register_display_provider(my_display)
FerroCAD.register_interaction_provider(my_interaction)

Registration is idempotent only for the same function object. There is no unregister, so a process should load a given display/interaction mod once.

Display

The provider

def my_display(obj):
    """The display mesh for ``obj``, or ``None`` if this mod does not own it."""
    if not hasattr(obj, "MyGeometry"):
        return None
    return {
        "positions": [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
        "triangles": [0, 1, 2],
        "triangle_parts": [0],
        "edges": [0, 1, 1, 2, 2, 0],
        "segment_parts": [0, 0, 0],
        "vertices": [],
        "vertex_parts": [],
        "parts": [],
        "color": [0.26, 0.55, 0.85, 1.0],
        "display": "shaded_edges",
    }

FerroCAD.display_mesh(obj) returns the first non-None result. It does not catch exceptions; a provider that raises breaks the mesh sync. Convert your own geometry in a try/except and return None on failure (the toy does this).

The mesh fields

The dict is the ferrocad.geom.Tessellation shape plus two presentation fields.

FieldShapeMeaning
positions[[x, y, z], ...]vertex positions used as given; the shell applies no object transform, so bake Placement in
normals[[x, y, z], ...]optional; used only when its length equals positions; otherwise the renderer flat-shades
triangles[i, j, k, ...]flat indices, three per triangle
triangle_parts[part_id, ...]one part id per triangle; a missing entry means part 0
edges[i, j, ...]flat line indices, two per segment; one run per topological edge
segment_parts[part_id, ...]one part id per segment
vertices[position_index, ...]the position indices of the topological vertices
vertex_parts[part_id, ...]one part id per entry in vertices
parts["Face1", "Edge1", ...]part id to sub-element name; the id is the index
color[r, g, b, a]optional RGBA in linear 0..1; applied to the surface and the edges
display"shaded", "wireframe", "shaded_edges"optional draw mode; absent or unknown means "shaded_edges"

Notes:

  • parts is what turns a pick into Face6/Edge3. It is a name table, not a per-vertex array; the per-primitive arrays carry the ids.
  • The shell flattens triangle_parts/segment_parts/vertex_parts into a per-position part id, so a position should belong to one part.
  • vertices/vertex_parts are topology data. The current display path records them for picking but does not draw points from them (show_vertices is off in DisplaySync).
  • color and display are presentation only; they never affect the document.

How the shell consumes it

ferrocad_shell.stage_meshes():

  1. reads FreeCAD.ActiveDocument;
  2. skips an object that a feature consumes through its Base link;
  3. calls FerroCAD.display_mesh(obj);
  4. adds object (the internal name), label, and entity (the object’s 1-based position in doc.Objects);
  5. returns a JSON list.

A mesh with empty positions is skipped when the shell builds the stage. An empty list means “nothing to show”, and the shell keeps its sample scene.

The Rust side (ferrocad_gpui::display::DisplaySync::build) turns each entry into a ShapeItem, applies display as the draw mode and color as the surface/edge colour, and keeps the entity -> object map a pick needs.

import json
import ferrocad_shell
import ferrocad_toy

ferrocad_toy.bootstrap("Display")
mesh = json.loads(ferrocad_shell.stage_meshes())[0]
print(mesh["object"], len(mesh["positions"]), len(mesh["triangles"]) // 3)

Output:

Slab 24 12

Interaction

The provider

def my_interaction(obj, manipulator, action, element, values):
    """Apply a manipulator commit, or return ``None`` if it is not ours."""
    if manipulator != "my_height" or action != "set":
        return None
    doc = obj.Document
    doc.openTransaction("Set height")
    try:
        obj.Height = float(values[0])
        doc.recompute()
        doc.commitTransaction()
    except Exception as exc:
        doc.abortTransaction()
        return {"ok": False, "error": str(exc)}
    return {"ok": True}
ArgumentMeaning
objthe resolved document object
manipulatorthe manipulator registry id, for example "toy_height", "transform", "dress_up"
actionthe operation within it, for example "set", "apply", "fillet", "chamfer"
elementthe sub-element name ("Face6", "Edge3") or ""
valuesthe numbers in the manipulator’s documented order, as floats

FerroCAD.interaction(obj, manipulator, action, element="", values=None) asks each registered provider in order and returns the first non-None result. The edit belongs to the provider: it opens the transaction, creates or edits the feature, and recomputes. The shell only marshals.

How the shell calls it

ferrocad_shell.interaction(object_name, manipulator, action, element, values_json):

  1. resolves object_name in the active document;
  2. parses values_json as a list of numbers;
  3. calls FerroCAD.interaction(...);
  4. returns the provider’s dict as JSON, or {"ok": false, "error": "no module handles <manipulator> <action>"} when no provider claims it.
import json
import ferrocad_shell
import ferrocad_toy

ferrocad_toy.bootstrap("Display")
print(ferrocad_shell.interaction("Slab", "toy_height", "set", "", json.dumps([42.0])))

Output:

{"ok": true}

The shell’s error paths are also JSON: no active document, no object with that name, or values that are not numbers.

A provider that raises is not isolated here. Return {"ok": False, "error": ...} instead of raising.

Overlays are not display providers

An overlay is a Coin node registered with FerroCADGui.addOverlay(node) and drawn beside the document scene, not a per-object mesh. addOverlay extracts the subtree immediately (the “pull” bridge), so a later in-place edit does not reach the copy: remove and re-add the node after changing it. removeOverlay keys on Python object identity.

import FerroCADGui

FerroCADGui.addOverlay(root)
FerroCADGui.removeOverlay(root)

This path requires a pivy.coin facade that provides addOverlay and removeOverlay. In a build without it, addOverlay raises. A registered overlay draws regardless of the grid_overlay view flag; that flag only hides the native ground. mods/GridOverlayCoin is the complete example.

Next

A minimal mod

Status: guide (2026-10-22).

This is a complete mod: one command that creates a box, and one display provider that draws it. It uses only the current facade and needs no GPU.

Directory

mods/
  MinimalBox/
    Init.py
    InitGui.py

Init.py is the non-GUI hook. This mod has nothing to register there:

"""Non-GUI startup hook. This mod registers nothing here."""

InitGui.py

"""A minimal FerroCAD mod: one command and one display provider."""

import FreeCAD
import FreeCADGui
import FerroCAD

COMMAND = "Minimal_AddBox"
SIZE = 10.0

_CORNERS = (
    (-0.5, -0.5, 0.0), (0.5, -0.5, 0.0), (0.5, 0.5, 0.0), (-0.5, 0.5, 0.0),
    (-0.5, -0.5, 1.0), (0.5, -0.5, 1.0), (0.5, 0.5, 1.0), (-0.5, 0.5, 1.0),
)
_FACES = (
    (0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4),
    (1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7),
)


def _box_mesh(obj):
    """Draw an object with a Length property; return None for anything else."""
    length = getattr(obj, "Length", None)
    if length is None:
        return None
    try:
        size = float(length)
        placement = getattr(obj, "Placement", None)
        corners = [
            FreeCAD.Vector(x * size, y * size, z * size)
            for x, y, z in _CORNERS
        ]
        if placement is not None:
            corners = [placement.multVec(corner) for corner in corners]
        positions, triangles, edges = [], [], []
        for face in _FACES:
            base = len(positions)
            for index in face:
                corner = corners[index]
                positions.append((corner.x, corner.y, corner.z))
            triangles.extend((base, base + 1, base + 2, base, base + 2, base + 3))
            edges.extend(
                (base, base + 1, base + 1, base + 2,
                 base + 2, base + 3, base + 3, base)
            )
        return {
            "positions": positions,
            "triangles": triangles,
            "triangle_parts": [0] * (len(triangles) // 3),
            "edges": edges,
            "segment_parts": [0] * (len(edges) // 2),
            "vertices": [],
            "vertex_parts": [],
            "parts": [],
            "color": [0.85, 0.55, 0.20, 1.0],
            "display": "shaded_edges",
        }
    except Exception:
        return None


def _add_box():
    doc = FreeCAD.ActiveDocument
    if doc is None:
        return {"status": "open a document first"}
    doc.openTransaction("Add box")
    try:
        box = doc.addObject("App::FeaturePython", "Box")
        box.addProperty("App::PropertyLength", "Length", "Box", "Edge length")
        box.addProperty("App::PropertyPlacement", "Placement", "Box", "Placement")
        box.Length = SIZE
        box.Placement = FreeCAD.Placement()
        doc.recompute()
        doc.commitTransaction()
    except Exception as exc:
        doc.abortTransaction()
        return {"status": "add box failed: %s" % exc}
    return {"status": "created %s" % box.Name}


class AddBoxCommand:
    def GetResources(self):
        return {
            "MenuText": "Add Minimal Box",
            "ToolTip": "Add a 10 mm box",
            "Group": "Minimal",
            "CmdType": "ForEdit",
        }

    def IsActive(self):
        return FreeCAD.ActiveDocument is not None

    def Activated(self):
        return _add_box()


FreeCADGui.removeCommand(COMMAND)  # make a reload safe
FreeCADGui.addCommand(COMMAND, AddBoxCommand())
FerroCAD.register_display_provider(_box_mesh)

What each piece does

PieceContract
AddBoxCommand.GetResourcesthe label, tooltip, group and ForEdit metadata the actions bar reads
IsActiveenabled only when a document is open
Activatedone named transaction; returns {"status": ...}
_box_meshreturns a mesh for an object with Length, None for everything else
positions24 entries: six quads, four corners each, already placed by Placement
triangles12 triangles (two per face)
edges24 segments (four per face)
triangle_parts / segment_partsone untagged part (0), so a click selects the whole object
colorRGBA in 0..1; the shell uses it for the surface and edges
display"shaded_edges"; "shaded" and "wireframe" are the other modes

The provider claims any object with a Length property. That is deliberate for a minimal example; a real mod should use a private marker property or a proxy type so it does not draw someone else’s object. In the app’s sample document, Params also has a Length, so this provider claims it too.

Run it headlessly

Put the directory at mods/MinimalBox/, then:

import importlib.util
import json
import pathlib

import FreeCAD
import FreeCADGui
import ferrocad_shell

path = pathlib.Path("mods/MinimalBox/InitGui.py").resolve()
FreeCADGui.removeCommand("Minimal_AddBox")
spec = importlib.util.spec_from_file_location("MinimalBox_InitGui", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

print("registered:", "Minimal_AddBox" in FreeCADGui.listCommands())

FreeCAD.newDocument("Minimal")
print(json.loads(ferrocad_shell.run_command("Minimal_AddBox")))

mesh = json.loads(ferrocad_shell.stage_meshes())[0]
print(
    mesh["object"],
    len(mesh["positions"]),
    len(mesh["triangles"]) // 3,
    mesh["color"],
    mesh["display"],
)

FreeCAD.closeDocument("Minimal")
FreeCADGui.removeCommand("Minimal_AddBox")

Output:

registered: True
{'ok': True, 'status': 'created Box'}
Box 24 12 [0.85, 0.55, 0.2, 1.0] shaded_edges

The app loads the same file at boot from any configured mods directory. There is no registration step and no build step.

Extend it

  • Ask for the size. Return a dialog spec from Activated with a quantity field named Length, and create the box in on_accept (commands.md).
  • Pick faces. Give each face a distinct triangle_parts id and fill parts with names such as "Face1"; a pick then reports Face1 (display-and-interaction.md).
  • Let the user drag it. Register an interaction provider for a manipulator id and write the new Length/Placement in one transaction.
  • Add a backend. If the mod implements a shared capability, declare it with FerroCADGui.registerProvider instead of keeping a private on/off flag (providers.md).

Next

Mod anatomy

Status: guide (2026-10-22).

A FerroCAD mod is a directory of Python scripts loaded at boot. This page is the lifecycle and the failure model. The command, display, interaction and provider contracts have their own pages.

Directory

mods/
  MyMod/
    Init.py          # non-GUI hook
    InitGui.py       # GUI hook
    Resources/
      icon.svg
    package.xml      # optional upstream metadata; see below

Both scripts are optional. A directory with neither contributes nothing. The current authored mods (mods/GridOverlay, mods/GridOverlayCoin) use a one-line Init.py and put everything in InitGui.py.

Init.py and InitGui.py

ScriptConventional roleIn FerroCAD today
Init.pynon-GUI registration: document types, display/interaction providersexecuted at boot
InitGui.pycommands, feature providers, overlaysexecuted at boot, including when FreeCAD.GuiUp == 0

FerroCAD does not skip InitGui.py in a headless process. The loader runs both scripts unconditionally; the headless facade still has the command registry and the provider registry.

Each script is executed, not imported as a package:

namespace = {"__file__": path, "__name__": f"{name}.{script[:-3]}"}
exec(compile(source, path, "exec"), namespace)

Consequences:

  • __file__ is the script path and __name__ is "MyMod.InitGui".
  • The two scripts get separate namespaces; a global in Init.py is not visible in InitGui.py.
  • The script is not added to sys.modules. import MyMod.InitGui is not the loader path.
  • Relative imports do not work. If a mod needs a sibling module, add its directory to sys.path before importing it.
  • The loader catches exceptions per script. A failing Init.py does not prevent InitGui.py from running.

package.xml

package.xml is FreeCAD package metadata. FerroCAD’s loader does not read it and no code path in this tree references it. Ship one only if the same directory is also published to upstream FreeCAD; it has no effect on loading, commands or the UI here. The authored mods in mods/ do not include one.

For reference, the upstream format starts like this:

<?xml version="1.0" encoding="utf-8"?>
<package format="1" xmlns="https://wiki.freecad.org/Package_Metadata">
  <name>Minimal Box</name>
  <description>A minimal FerroCAD mod.</description>
  <version>0.1.0</version>
  <date>2026-10-22</date>
  <maintainer email="you@example.com">Your Name</maintainer>
  <license file="LICENSE">MIT</license>
</package>

Treat that as upstream-compatible metadata, not as something this build validates.

Registration at import time

A mod registers as a side effect of running its script. There is no register() callback, no workbench activation, and no lazy load.

# InitGui.py
import FreeCAD
import FreeCADGui
import FerroCAD
import FerroCADGui

FreeCADGui.addCommand("MyMod_DoThing", DoThingCommand())
FerroCAD.register_display_provider(display_my_object)
FerroCADGui.registerProvider("my_feature", "my_backend", "My Backend")

Rules that follow from the registries:

  • addCommand rejects a duplicate id with ValueError. A mod that may be executed twice in one process should call FreeCADGui.removeCommand(id) first; the minimal mod and mods/GridOverlayCoin do this.
  • registerProvider replaces an existing (feature, backend) record, so re-registration is reload-safe.
  • register_display_provider and register_interaction_provider are idempotent only for the same function object. Re-executing a script creates new function objects and appends new providers. There is no unregister.
  • Registration should be cheap. Do not draw, open a document, or start a transaction at import time.
  • A script that raises after registering something leaves the earlier registrations in place. Registration is immediate, not transactional.

Init.py and InitGui.py are not given a teardown call. If you attach a document observer or open another resource, keep it in a module global and shut it down yourself if the module is reloaded (the Coin mod’s GridManager does this).

Lifecycle

  1. The host configures Python, including python/ on sys.path.
  2. The host chooses the mods directories: HostConfig.mods_paths, else FERROCAD_MODS_PATH, else a discovered mods/ beside python/.
  3. ferrocad_shell.load_workbenches(dirs_json) scans each directory’s immediate subdirectories in sorted order.
  4. For each subdirectory it executes Init.py then InitGui.py, if present.
  5. The app boots. Commands are listed on demand, display meshes are requested on a document sync, interactions on a manipulator commit, a provider switch on Mods_Settings, and a provider refresh after a feature-level state change such as the grid toggle.
  6. There is no file watcher or hot reload. An edited script takes effect on the next process start; a test re-executes it deliberately.

The JSON result of a load is:

{
  "loaded": ["MyMod/Init.py", "MyMod/InitGui.py"],
  "errors": [
    {"workbench": "OtherMod", "script": "InitGui.py", "traceback": "..."}
  ]
}

The shell prints workbenches: loaded N script(s) and one console line per error; the traceback goes to stderr.

The isolation rule

One broken mod must not take down the application. The rule is enforced at different strengths:

FailureBehaviour
A script raises at loadcaught by the loader; the script is recorded under errors; other scripts still run; the app boots
GetResources() raisestreated as {}; reported once; the command keeps its id
IsActive() raisesthat command is disabled; reported once; other commands are unaffected
IsChecked() raisestreated as False; reported once
Activated() raises through the shell bridge{"ok": false, "error": "..."}; the app keeps running
A provider activate/deactivate/refresh raisescaught, reported once per (feature, backend, hook), listed under errors; the switch or refresh continues
A display provider raisesnot caught by FerroCAD.display_mesh or stage_meshes
An interaction provider raisesnot caught by FerroCAD.interaction or ferrocad_shell.interaction
addOverlay failsnot caught; it raises if the Coin facade is missing

So the rule is: the loader and the registries isolate their calls, but your display and interaction providers are called directly. The convention is to return None for work you do not own, and to wrap your own conversion in try/except and return None (or {"ok": False}) rather than raise. The toy’s _display_mesh does exactly that.

IsActive and Activated must also tolerate FreeCAD.ActiveDocument is None. A command that needs a document should return False from IsActive or a {"status": ...} from Activated.

Next

Feature providers

Status: guide (2026-10-22).

A feature is a named capability with more than one possible implementation. A provider (or backend) is one implementation of that feature. The provider registry keeps exactly one active backend per feature and persists the choice.

The reference pair is the grid:

Modfeaturebackendlabelpriority
mods/GridOverlaygridferrocadFerroCAD (native)10
mods/GridOverlayCoingridcoinCoin (WGSL)0

Both register from InitGui.py. Only one draws; Mods_Settings switches.

Declaring a provider

import FerroCADGui

FerroCADGui.registerProvider(
    "grid",                     # feature
    "coin",                     # backend id
    "Coin (WGSL)",              # label shown by Mods_Settings
    command="GridOverlayCoin_Settings",
    priority=0,
    activate=activate,          # make this backend the drawn one
    deactivate=deactivate,      # undo activate
    refresh=refresh,            # re-read shared state, no switch
)
ArgumentMeaning
featurenon-empty string; what is implemented
backendnon-empty string; this implementation’s id
labelthe user-facing name; falls back to backend
commandthe command id this backend owns, or None
prioritylarger wins the default; equal priority is broken by earliest registration
activateoptional zero-argument callback: become the drawn backend
deactivateoptional zero-argument callback: undo activate
refreshoptional zero-argument callback: shared state changed, re-read it

activate, deactivate and refresh are optional and must be callable if given. activate and refresh never run at registration; deactivate runs there only for the takeover case below. registerProvider returns the new backend’s manifest.

Registering the same (feature, backend) again replaces the record without moving it in the registration order. That makes a mod reload safe.

Takeover. If a registration changes the active backend — a higher priority, or a saved choice that names the new backend — the registry runs the outgoing backend’s deactivate immediately. The incoming backend is still not activated until the first setActiveProvider, so activate stays out of import time.

Settings the provider advertises

A provider can declare its knobs, so the shell has one settings surface whichever backend is active:

FerroCADGui.registerProvider(
    "grid", "coin", "Coin (WGSL)",
    command="GridOverlayCoin_Settings",      # fallback: a backend with no schema
    settings=[
        {"name": "enabled", "label": "Enabled", "kind": "bool", "default": True},
        {"name": "color", "label": "Colour", "kind": "color", "default": "#94a3b8"},
        {"name": "spacing", "label": "Spacing", "kind": "quantity",
         "unit": "mm", "default": 10.0, "min": 0.1, "max": 1000.0},
    ],
)
KeyMeaning
namestable key; what setSetting addresses
labelthe row’s text
kindtext, bool, int, float, quantity, enum, color
defaultshown until a value is set; not written at registration
min / maxnumeric range, enforced by the setter
unitsuffix for quantity ("mm"); a unit string is accepted and converted
choicesthe closed set for enum
get / setoptional callables for computed knobs; get without set is read-only

Values live in User parameter:BaseApp/Preferences/Mods/<feature>/<backend>, one key per setting, unless a get/set pair is supplied. A malformed schema is rejected at registration; an unknown kind renders read-only instead of breaking the pane.

FerroCADGui.settings("grid")                  # rows for the active backend
FerroCADGui.setSetting("grid", "spacing", 5)  # validate, store, refresh the backend

The rows use the property pane’s contract (name, label, kind, value, editable, …), so the tree’s Settings/Grid node renders them without a second editor. setSetting enforces the range, the choices and the kind’s type; a rejected value changes nothing. On success it runs the backend’s refresh, so a provider that draws from these values sees them immediately.

A provider with no settings keeps its command as its settings entry: that is the upstream-compatible fallback, and the reason a ported mod still works unchanged.

Which backend is active

In order:

  1. the persisted choice at FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mods") → Providers/<feature>, if it names a registered backend;
  2. otherwise the highest priority;
  3. among equal priorities, the earliest registered backend.

A saved value that names a removed or renamed backend is ignored, not an error.

import FerroCADGui

FerroCADGui.activeProvider("grid")     # "ferrocad" (the declared default)
FerroCADGui.providers("grid")          # manifests for both backends
FerroCADGui.providers()                # every feature

A manifest is JSON-safe:

{
    "feature": "grid",
    "backend": "ferrocad",
    "label": "FerroCAD (native)",
    "command": "GridOverlay_Settings",
    "priority": 10,
    "active": True,
}

The callbacks stay private, which is why the manifest can cross the JSON boundary.

Switching

result = FerroCADGui.setActiveProvider("grid", "coin")

setActiveProvider(feature, backend):

  1. persists the choice;
  2. runs the outgoing backend’s deactivate(), if any;
  3. runs the incoming backend’s activate(), if any;
  4. records the incoming backend as applied.

The outgoing backend is the applied one when there is one, otherwise the declared active one. That is what makes the first switch restore the default it left, even if that default’s activate never ran. Selecting the already-declared backend is the bootstrap case: its activate runs once; later selections are inert.

The callbacks may return the same dict a command may return. view is merged across the switch and returned under view; status is folded into status. Failures are caught, reported once per (feature, backend, hook), and listed under errors.

{
    "ok": True,
    "feature": "grid",
    "backend": "coin",
    "previous": "ferrocad",      # declared before the call
    "view": {"grid": False, ...},
    "status": "grid: coin is the active provider",
    "errors": [],
}

An unknown feature or backend raises ValueError. A callback failure does not: a failing deactivate does not skip the incoming activate.

unregisterProvider(feature, backend) drops a record and returns whether it existed. It does not run deactivate; the caller owns the effects. It leaves the persisted choice alone, so a re-registered backend keeps its selection.

Refresh, without switching

A feature’s state can change without a backend switch. refreshProviders(feature) asks the active backend only to re-read it:

FerroCADGui.refreshProviders("grid")

It returns:

{
    "ok": True,
    "feature": "grid",
    "backend": "coin",          # None when no provider is registered
    "view": {"grid_overlay": True},
    "status": "",
    "errors": [],
}

The refresh callback is the counterpart of activate/deactivate for “shared state changed, re-read it”. It runs only when asked, never at registration or on a switch. An absent callback, or no provider for the feature, is a no-op. Errors are reported and collected, never raised.

The feature owns the switch, not the provider

This is the rule that keeps two implementations from disagreeing about “on”:

  • The feature owns one master switch and the command that writes it.
  • A provider’s own knobs are for appearance, not for the master switch.
  • activate/deactivate select an implementation; they are not the only place the feature is turned on.
  • A provider that draws from its own store must implement refresh so a feature-level change reaches it.
  • An inactive backend checks activeProvider(feature) before drawing and in Activated, and reports why it refused. IsActive stays True so the entry remains visible.

The grid is the worked example:

App_ToggleGrid
  -> flips FerroCAD.Grid.enabled            (the one switch)
  -> builds the request from FerroCAD.Grid.apply()
  -> calls FerroCADGui.refreshProviders("grid")
       -> the active backend's refresh() re-reads FerroCAD.Grid.enabled
       -> the Coin backend re-binds its overlay and returns
          {"view": {"grid_overlay": <enabled>}}
  -> merges that grid_overlay claim into the request and returns it

mods/GridOverlayCoin now reads FerroCAD.Grid.enabled for enabled when the semantic API exists (upstream FreeCAD falls back to its private SETTINGS knob). Its colour, opacity, spacing and shadow knobs stay in its own parameter group. The native backend reads the same switch directly in apply().

The internal note docs/workbenches/mod-providers.md covers the selection and switch contract; this page adds the current refresh hook and the feature-owned switch rule.

Checklist for a backend

  • Register from InitGui.py with a unique (feature, backend).
  • Give Mods_Settings a unique label within the feature.
  • Check FerroCADGui.activeProvider(feature) before drawing and in Activated; return a {"status": ...} naming the active backend when it is not you.
  • Make activate idempotent and safe with no document.
  • Make deactivate undo only what activate added; leave user settings alone.
  • Implement refresh if the feature’s master switch lives outside your store.
  • Keep appearance settings in your own ParamGet group.

Runnable registry example

import FerroCADGui

calls = []

def activate_a():
    calls.append("activate")
    return {"status": "A active"}

def refresh_a():
    calls.append("refresh")
    return {"view": {"grid": True}}

FerroCADGui.registerProvider(
    "demo", "a", "Backend A", priority=10,
    activate=activate_a, refresh=refresh_a,
)
FerroCADGui.registerProvider("demo", "b", "Backend B", priority=0)

print(FerroCADGui.providers("demo"))
print(FerroCADGui.activeProvider("demo"))
print(FerroCADGui.refreshProviders("demo"))
print(calls)

for entry in FerroCADGui.providers("demo"):
    FerroCADGui.unregisterProvider("demo", entry["backend"])

Limits

  • The persisted choice is in the in-memory ParamGet shim today; there is no cross-session persistence.
  • Labels should be unique within a feature. Mods_Settings maps a value back to a backend by label first, then by id.
  • The registry says which backend should be active. A third-party mod that ignores activeProvider can still draw; the contract is enforced by the mods that follow it.
  • There is no native settings panel; Mods_Settings uses the ordinary dialog protocol.
  • refreshProviders runs only the active backend. It does not switch, persist, or call activate.

Settings and parameters

Status: guide (2026-10-22).

Parameters are FerroCAD’s persistence layer for configuration. A mod reads them when it builds a dialog or a draw path, writes them when the user accepts a dialog, and returns a {"view": ...} request to make the change visible.

FreeCAD.ParamGet

import FreeCAD

prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/MyMod")
prefs.SetBool("Enabled", True)
prefs.SetFloat("Opacity", 0.5)
prefs.SetString("Color", "#94a3b8")

ParamGet(path="", create=True) returns a ParameterGrp. The path is split at the first colon: the part before it is the root, the part after it is the group. The create argument is accepted but the shim always creates the group.

The store is in memory today. Values survive ParamGet calls and a mod reload in the same interpreter, but not a process restart. Cross-session persistence is a parameter-backend gap, not something a mod should work around.

ParameterGrp

MemberBehaviour
GetInt(name, default=0) / SetInt / RemIntinteger values
GetUnsigned(name, default=0) / SetUnsigned / RemUnsigned32-bit unsigned; stored in the same slot as int
GetBool(name, default=False) / SetBool / RemBoolbooleans
GetFloat(name, default=0.0) / SetFloat / RemFloatfloats
GetString(name, default="") / SetString / RemStringstrings
GetGroup(name)a child group; creates it
GetGroupName()the last path component
GetGroups()the immediate child group names, sorted
HasGroup(name)whether the group was created
GetInts() / GetBools() / GetFloats() / GetStrings()the names in this group with a value of that type (upstream FreeCAD’s shape)
Clear()removes the values in this group and its descendants
RemGroup(name)removes a child group, its values and descendants — unless a live handle still references it (upstream BaseTests.testGroup)
Export(path) / Import(path)JSON file, not the upstream FreeCAD parameter format

Clear() removes values, not group names; HasGroup can still be true afterwards. Use RemGroup when the group itself must go.

Export writes a JSON object whose keys are "<relative group>\t<name>\t<kind>"; Import reads that shape back. These are ordinary file operations, not a FreeCAD .FCStd parameter file.

The worked example: FerroCAD.Grid

FerroCAD.Grid is the semantic grid settings object. It is resolved lazily as an attribute of the FerroCAD module; import FerroCAD.Grid is not the API.

import FerroCAD

grid = FerroCAD.Grid
grid.enabled = True

The values live in FreeCAD.ParamGet("User parameter:BaseApp/Preferences/View"):

PropertyParameterDefaultValidation / meaning
enabledGridEnabledFalseshow the world grid’s ground layer
colorGridColor"#94a3b8"#rgb or #rrggbb; stored canonical lowercase
opacityGridOpacity1.0multiplier over the token alphas, 0..1
spacingGridMinorStep + GridMajorStep10.0 / 100.0getter is minor; setter writes the next decade as major
minorGridMinorStep10.0positive, millimetres
majorGridMajorStep100.0positive, millimetres
radiusGridRadius0.0fixed extent in millimetres; 0 is camera-derived
axis_crossGridAxisCrossTruethe world triad
shadowsGridShadowFalsepersisted; nothing draws it yet
shadow_opacityGridShadowOpacity0.30persisted; nothing draws it yet, 0..1

Setters validate and raise ValueError on bad input; they do not silently clamp.

apply() is the bridge to the viewport. It returns the request a command returns:

{
    "view": {
        "grid": True,
        "axis_cross": True,
        "grid_minor": 2.0,
        "grid_major": 20.0,
        "grid_radius": 0.0,
        "grid_color": "#123456",
        "grid_alpha": 0.5,
    }
}

It raises if the major step is below the minor one. The shadow preferences are not included because nothing consumes them.

snapshot() is for seeding a dialog. With FreeCAD.GuiUp and a FerroCADGui.ViewState, it reads the shell’s mirror of what is actually drawn; otherwise it reads the store. It also returns shadows and shadow_opacity, and reports grid_radius as None when the stored value is 0.

reset() clears the whole BaseApp/Preferences/View group, not only the grid keys.

A runnable read/write

import FreeCAD
import FerroCAD

grid = FerroCAD.Grid
grid.reset()
grid.enabled = True
grid.color = "#123456"
grid.opacity = 0.5
grid.spacing = 2.0

print(grid.apply())
print(
    FreeCAD.ParamGet("User parameter:BaseApp/Preferences/View")
    .GetFloat("GridMinorStep", 0.0)
)

Output:

{'view': {'grid': True, 'axis_cross': True, 'grid_minor': 2.0, 'grid_major': 20.0, 'grid_radius': 0.0, 'grid_color': '#123456', 'grid_alpha': 0.5}}
2.0

Advertised settings (providers)

If the mod owns a feature provider (see providers.md), do not write another dialog: declare the knobs in registerProvider(settings=[...]).

import FerroCADGui

FerroCADGui.registerProvider(
    "grid", "mymod", "My grid",
    settings=[
        {"name": "spacing", "label": "Spacing", "kind": "quantity",
         "unit": "mm", "default": 10.0, "min": 0.1},
    ],
)

FerroCAD stores each value under User parameter:BaseApp/Preferences/Mods/<feature>/<backend> and renders the rows under Settings/<Feature>; setSetting validates and then runs the backend’s refresh. That is what gives the user one surface across backends instead of a different dialog per mod. A provider with no settings keeps its command as its settings entry.

The mod pattern

Use one group per mod, for example User parameter:BaseApp/Preferences/Mod/MyMod:

import FreeCAD

GROUP = "User parameter:BaseApp/Preferences/Mod/MyMod"

def _prefs():
    return FreeCAD.ParamGet(GROUP)

def apply_dialog(values):
    prefs = _prefs()
    if "Opacity" in values:
        prefs.SetFloat("Opacity", float(values["Opacity"]))
    return {"view": ...}

Rules that keep settings and drawing consistent:

  1. Read in Activated to seed the dialog, and in the draw path if the provider draws from its own store.
  2. Write in on_accept, then return the {"view": ...} request that makes the shell repaint.
  3. Do not use a parameter as a second master switch for a feature that already has one. The feature owns the switch (providers.md); appearance knobs are yours.
  4. Validate before writing, so a rejected value leaves the store unchanged.
  5. Do not assume persistence across runs.

mods/GridOverlay is the reference: its on_accept writes FerroCAD.Grid and returns FerroCAD.Grid.apply(). mods/GridOverlayCoin keeps its appearance knobs in its own group and reads FerroCAD.Grid.enabled for the shared switch when that API exists.

Next

Testing a mod

Status: guide (2026-10-22).

Almost everything a mod does can be driven without a window or a GPU. The application’s own tests do exactly that, by executing InitGui.py and calling the same functions the shell calls.

Run the suite with:

PYTHONPATH=python python3 -m unittest discover -s tests -v

What needs no GPU

You can testHow
import-time registrationload InitGui.py by path, then inspect FreeCADGui.listCommands()
command resources and enablementFreeCADGui._command_list(), FreeCADGui.listCommands()
running a commandferrocad_shell.run_command(id) or FreeCADGui.runCommand(id)
dialogs, accept and cancelferrocad_shell.dialog_accept / dialog_cancel
provider selection and switchingregisterProvider, activeProvider, setActiveProvider, refreshProviders
selection-dependent IsActiveferrocad_shell.set_selection(...)
view-dependent IsCheckedferrocad_shell.set_view_state(...)
display meshesFerroCAD.display_mesh(obj) or ferrocad_shell.stage_meshes()
interaction commitsferrocad_shell.interaction(...)
overlay registrationneeds a pivy.coin facade; the Coin tests install a stub
shader compilation, drawing, the windowneeds a GPU and a viewport

Load the mod by path

Do not depend on sys.path order. Load the file directly:

import importlib.util
import pathlib

import FreeCADGui

MOD = pathlib.Path("mods/MinimalBox/InitGui.py").resolve()

def load_mod():
    FreeCADGui.removeCommand("Minimal_AddBox")   # reload-safe
    spec = importlib.util.spec_from_file_location(
        "MinimalBox_InitGui", MOD
    )
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    return module

This is the pattern in tests/test_grid_overlay_fork.py:

import sys

before = set(sys.modules)
FreeCADGui.removeCommand(COMMAND)
spec = importlib.util.spec_from_file_location("GridOverlay_InitGui", MOD_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

It also records the modules the mod imported, so a test can assert that loading the mod did not pull in Qt or Coin.

If the mod is already registered in this interpreter, removeCommand is what avoids the registry’s duplicate-id ValueError. A provider re-registration is safe; a display/interaction provider is not removable, so load that module once per process.

A test class

import importlib.util
import json
import pathlib
import unittest

import FreeCAD
import FreeCADGui
import ferrocad_shell

MOD = pathlib.Path("mods/MinimalBox/InitGui.py").resolve()
COMMAND = "Minimal_AddBox"

class MinimalModTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        FreeCADGui.removeCommand(COMMAND)
        spec = importlib.util.spec_from_file_location("MinimalBox_InitGui", MOD)
        cls.mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(cls.mod)

    @classmethod
    def tearDownClass(cls):
        FreeCADGui.removeCommand(COMMAND)

    def setUp(self):
        for name in list(FreeCAD.listDocuments()):
            FreeCAD.closeDocument(name)
        self.doc = FreeCAD.newDocument("Test")

    def tearDown(self):
        FreeCADGui._cancel_all()
        FreeCAD.closeDocument(self.doc.Name)

    def test_the_command_is_registered(self):
        self.assertIn(COMMAND, FreeCADGui.listCommands())

    def test_the_command_creates_a_displayed_box(self):
        result = json.loads(ferrocad_shell.run_command(COMMAND))
        self.assertTrue(result["ok"], result)

        meshes = json.loads(ferrocad_shell.stage_meshes())
        self.assertEqual(meshes[0]["object"], "Box")
        self.assertEqual(len(meshes[0]["triangles"]) // 3, 12)

The class loads the module once, so the display provider is registered once. tearDownClass removes the command; the provider stays for the process, which is the best the facade allows.

Push the shell’s state

The shell owns selection and presentation state. In a test, push them yourself.

import json
import FreeCADGui
import FerroCADGui
import ferrocad_shell
import ferrocad_toy

ferrocad_toy.bootstrap("State")
ferrocad_shell.set_selection(
    json.dumps([{"object": "Slab", "elements": ["Face1"]}])
)
assert FreeCADGui.Selection.getSelection()[0].Name == "Slab"
assert FreeCADGui.Selection.getSelectionEx()[0][1] == ["Face1"]

ferrocad_shell.set_view_state(
    json.dumps({"grid": True, "projection": "ortho"})
)
assert FerroCADGui.ViewState.grid is True
assert FerroCADGui.ViewState.projection == "ortho"
  • set_selection payload: [{"object": name, "elements": [...]}, ...]. Objects that are no longer in the document are pruned when the snapshot is resolved.
  • set_view_state payload: any of grid, axis_cross, projection, grid_minor, grid_major, grid_radius. Missing keys use defaults (grid=False, axis_cross=True, projection="persp", grid_minor=10.0, grid_major=100.0, grid_radius=None).

Clear the selection at the end with ferrocad_shell.set_selection("[]") so the next test does not inherit it.

Run commands and dialogs

CallReturns
FreeCADGui.runCommand(id)the raw result of Activated; raises KeyError for an unknown id
FreeCADGui._run(id)the shell payload {ok, status?, value?, view?, tool?, dialog?, token?}; never raises
ferrocad_shell.run_command(id)the shell payload as a JSON string
import json
import ferrocad_shell

run = json.loads(ferrocad_shell.run_command("View_GridSettings"))
assert "token" in run

accepted = json.loads(ferrocad_shell.dialog_accept(json.dumps({
    "token": run["token"],
    "values": {"enabled": True, "color": "#00ff00"},
})))
assert accepted["ok"], accepted

View_GridSettings is used here because it is already registered; a mod command with an on_accept behaves identically.

Cancel instead:

import json
import ferrocad_shell

run = json.loads(ferrocad_shell.run_command("View_GridSettings"))
assert json.loads(ferrocad_shell.dialog_cancel(run["token"])) == {"ok": True}

tests/test_grid_overlay_fork.py uses FreeCADGui._run and FreeCADGui._accept directly; those are the private half of the same protocol.

Providers

tests/test_mod_providers.py is the reference. Its pattern:

  • load each mod once, then call the module’s register_provider() in setUp because the module object is cached and the registry may have been cleared;
  • clear the applied-backend map and the persisted choices between cases;
  • switch with setActiveProvider and assert the callbacks ran exactly once;
  • unregisterProvider every backend in tearDown.
import FreeCAD
import FreeCADGui

FreeCADGui._PROVIDER_APPLIED.clear()
FreeCAD.ParamGet(
    "User parameter:BaseApp/Preferences/Mods"
).GetGroup("Providers").Clear()

_PROVIDER_APPLIED is private; it is acceptable in tests that need to forget which backend was applied while keeping the registrations.

import FerroCADGui

FerroCADGui.registerProvider("demo", "a", "A", priority=10)
FerroCADGui.registerProvider("demo", "b", "B", priority=0)
assert FerroCADGui.activeProvider("demo") == "a"

result = FerroCADGui.setActiveProvider("demo", "b")
assert result["previous"] == "a"
assert result["ok"]

for entry in FerroCADGui.providers("demo"):
    FerroCADGui.unregisterProvider("demo", entry["backend"])

tests/test_mod_providers.py also asserts that callback errors are reported under errors and do not break the registry.

Display and interaction

import json
import FreeCAD
import FerroCAD
import ferrocad_shell
import ferrocad_toy

ferrocad_toy.bootstrap("Display")
ferrocad_toy.makeSlab(name="Box")

mesh = FerroCAD.display_mesh(FreeCAD.ActiveDocument.getObject("Box"))
assert len(mesh["triangles"]) // 3 == 12

response = json.loads(
    ferrocad_shell.interaction(
        "Slab", "toy_height", "set", "", json.dumps([42.0])
    )
)
assert response["ok"]
  • stage_meshes() needs an active document; it returns [] otherwise.
  • A mesh reaches the viewport only if a provider claims the object and positions is not empty.
  • interaction() returns {ok: false, error: "no module handles ..."} when no provider claims the request.

Cleanup checklist

A test that leaves global state behind makes the next test lie. At the end of a case (or class):

  • FreeCADGui.removeCommand(id) for every command the test registered.
  • FerroCADGui.unregisterProvider(feature, backend) for every provider.
  • FreeCADGui._cancel_all() to drop open dialog tokens.
  • FreeCADGui._PROVIDER_APPLIED.clear() and clear the Providers preference group if the test changed the provider selection.
  • FreeCAD.closeDocument(name) for every document.
  • FerroCAD.Grid.reset() if the test changed grid settings.
  • ferrocad_shell.set_selection("[]") if the test pushed a selection.
  • FerroCADGui.removeOverlay(node) if the test registered an overlay.

You cannot unregister a display or interaction provider. Keep those modules loaded once, or accept that each re-execution adds another provider.

What still needs a window

  • Overlay extraction needs pivy.coin with addOverlay/removeOverlay. tests/test_grid_overlay_coin.py installs a fake pivy.coin and asserts the bridge calls; the real draw is manual.
  • Shader compilation, the viewport, and anything that measures pixels need a GPU and cargo run -p ferrocad_gpui.
  • The headless facade has FreeCAD.GuiUp == 0; FerroCADGui.ViewState is still writable by set_view_state, and FerroCAD.Grid.snapshot() reads it only when GuiUp is truthy.