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.
| Page | What it shows | Requires |
|---|---|---|
| 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. | the FreeCAD facade; no workbench module and no GPU |
How-to guides
Task-oriented: do one specific thing.
| Page | What it shows | Requires |
|---|---|---|
| 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. | the FreeCAD facade; no workbench module and no GPU |
| 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. | the FreeCAD facade; no workbench module and no GPU |
| 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. | the FreeCAD facade; a writable temporary directory |
| 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. | 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 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. | 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 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. | 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
imagehook intools/workflows/_capture.pyis 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.pywrites it fromtools/workflows/00_first_model.py. Edit the workflow, not this page;python3 tools/userdocs.py --checkfails 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 back | Value |
|---|---|
| Version | 0.1.3 |
| Backend | ferrocad |
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 back | Value |
|---|---|
| Active document | FirstModel |
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 back | Value |
|---|---|
| Parameters.Length | 20 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 back | Value |
|---|---|
| Result.Width | 60 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 back | Value |
|---|---|
| Parameters.Length | 35 mm |
| Result.Width | 105 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 back | Value |
|---|---|
| Objects after reopening | [“Parameters”, “Result”] |
| Width after reopening | 105 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 back | Value |
|---|---|
| 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.pywrites it fromtools/workflows/01_documents_and_properties.py. Edit the workflow, not this page;python3 tools/userdocs.py --checkfails 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 back | Value |
|---|---|
| Active document | Demo |
| 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 back | Value |
|---|---|
| part.Length | 10 mm |
| part.Finish | Anodised |
| 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 back | Value |
|---|---|
| Error | “Bogus” is not part of the enumeration |
| part.Finish is unchanged | Anodised |
4. Close the document
Closing unregisters the document from the application.
FreeCAD.closeDocument('Demo')
| Read back | Value |
|---|---|
| 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.pywrites it fromtools/workflows/02_parametric_recompute.py. Edit the workflow, not this page;python3 tools/userdocs.py --checkfails 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 back | Value |
|---|---|
| Source.Length | 10 mm |
| Derived.Result | 20 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 back | Value |
|---|---|
| Source.Length | 15 mm |
| Derived.Result | 30 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 back | Value |
|---|---|
| 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.pywrites it fromtools/workflows/03_undo_and_persistence.py. Edit the workflow, not this page;python3 tools/userdocs.py --checkfails 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 back | Value |
|---|---|
| part.Length | 10 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 back | Value |
|---|---|
| part.Length after the edit | 25 mm |
| part.Finish after the edit | Anodised |
doc.undo(); doc.recompute()
| Read back | Value |
|---|---|
| part.Length after undo | 10 mm |
| part.Finish after undo | As machined |
doc.redo(); doc.recompute()
| Read back | Value |
|---|---|
| part.Length after redo | 25 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 back | Value |
|---|---|
| Open documents after close | [] |
reopened = FreeCAD.open(path)
reloaded = reopened.getObject("Part")
| Read back | Value |
|---|---|
| Reloaded name | Part |
| Reloaded Length | 25 mm |
| Reloaded Finish | Anodised |
4. Clean up
import shutil
FreeCAD.closeDocument(reopened.Name)
shutil.rmtree(os.path.dirname(path))
| Read back | Value |
|---|---|
| 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.pywrites it fromtools/workflows/04_toy_workbench.py. Edit the workflow, not this page;python3 tools/userdocs.py --checkfails 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 back | Value |
|---|---|
| Name | Wide |
| Width | 60 mm |
| Height | 30 mm |
| Placement.Base | Vector (10, 0, 0) |
| DisplayMode | ShadedWithEdges |
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 back | Value |
|---|---|
| Response | {“ok”: true} |
| Height after the grip | 42 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 back | Value |
|---|---|
| Response | {“ok”: true} |
| Placement.Base after the move | Vector (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 back | Value |
|---|---|
| Placement.Base after one undo (the move) | Vector (10, 0, 0) |
| Height after one undo | 42 mm |
slab.Document.undo()
slab.Document.recompute()
| Read back | Value |
|---|---|
| Height after two undos | 30 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())
| Command | Group | Kind | Enabled |
|---|---|---|---|
| App_Delete | Edit | ForEdit | false |
| App_Move | Edit | ForEdit | false |
| Mods_Settings | Edit | ForEdit | true |
| Toy_MakeSlab | Toy | ForEdit | true |
| Toy_Polyline | Toy | ForView | true |
The shell never links a workbench. Everything on this page reaches the document through
FreeCAD,FreeCADGuior the module hook the edition registered at import time.
8. Close the document
FreeCAD.closeDocument('Toy')
| Read back | Value |
|---|---|
| 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.pywrites it fromtools/workflows/05_selection_and_commands.py. Edit the workflow, not this page;python3 tools/userdocs.py --checkfails 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 back | Value |
|---|---|
| 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 back | Value |
|---|---|
| App_Delete, nothing selected | false |
ferrocad_shell.set_selection('[{"object": "Slab"}]')
| Read back | Value |
|---|---|
| Selection | [“Slab”] |
| App_Delete, one object selected | true |
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 back | Value |
|---|---|
| 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 back | Value |
|---|---|
| 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 back | Value |
|---|---|
| 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 back | Value |
|---|---|
| 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.pywrites it fromtools/workflows/06_grid_visibility.py. Edit the workflow, not this page;python3 tools/userdocs.py --checkfails 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 back | Value |
|---|---|
| FerroCAD.Grid.enabled | false |
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 back | Value |
|---|---|
| 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.enabled | true |
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 back | Value |
|---|---|
| App_ToggleGrid checked | {“App_ToggleGrid”: true} |
| FerroCADGui.ViewState.grid | true |
4. Turn it off again
second = json.loads(ferrocad_shell.run_command("App_ToggleGrid"))
ferrocad_shell.set_view_state(json.dumps(second["view"]))
| Read back | Value |
|---|---|
| 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.enabled | false |
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.mdfixed.
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.
| Module | Public names | What it is |
|---|---|---|
FreeCAD | 15 | Upstream-compatible surface: documents, objects, the base value types. |
FreeCAD.Base | 13 | The Base value types FreeCAD exposes as a submodule. |
FreeCAD.Units | 9 | Quantity parsing, units and number formatting. |
FreeCAD.Console | 5 | The console sink a workbench prints through. |
FreeCADGui | 20 | Upstream-compatible GUI surface: commands, selection and dialogs. |
FerroCAD | 11 | FerroCAD-only surface: the semantic grid and the module hooks. |
FerroCADGui | 15 | FerroCAD-only GUI surface: providers, settings, overlays and view state. |
ferrocad_shell | 26 | The 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.pyreads 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
| Function | Summary |
|---|---|
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).
| Member | Summary |
|---|---|
| apply() -> dict | The {"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() -> None | Clear the axis store (only the axis provider’s own group). |
| schema() -> list | A copy of :data:SETTINGS_SCHEMA (the rows the provider advertises). |
| snapshot() -> dict | The 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).
| Member | Summary |
|---|---|
active_color (property) | The hovered/active handle’s ink as a canonical lowercase #rrggbb. |
| apply() -> dict | The {"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() -> None | Clear the gizmo store (only the gizmo provider’s own group). |
ring_snap_deg (property) | The rotation ring’s snap increment in degrees. |
| schema() -> list | A 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() -> dict | The 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).
| Member | Summary |
|---|---|
| apply() -> dict | The {"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() -> None | Clear the grid store, including the pre-S4 legacy keys. |
| schema() -> list | A 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() -> dict | The 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.pyreads 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
| Function | Summary |
|---|---|
activeProvider(feature) | The active backend for feature, or None when none is registered. |
addDocumentListener(observer) -> None | Register observer for document lifecycle notifications. |
addOverlay(node) -> None | Register 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) -> list | The provider manifests, in registration order. |
refreshProviders(feature: str) -> dict | Ask 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) -> bool | Unregister observer; returns whether it was registered. |
removeOverlay(node) -> bool | Drop a previously registered overlay, keyed by Python object identity. |
setActiveProvider(feature: str, backend: str) -> dict | Make backend the active provider for feature and apply it. |
setSetting(feature: str, name: str, value) -> dict | Coerce, validate and store one setting, then refresh the provider. |
settings(feature: str, backend=None) -> list | The settings rows of feature’s active provider (or backend). |
unregisterProvider(feature: str, backend: str) -> bool | Drop a provider record; returns whether it existed. |
Object ViewState
The shell’s presentation state, pushed by the shell (read-only here).
| Member | Summary |
|---|---|
axis_cross (attribute) | — |
grid (attribute) | — |
grid_major (attribute) | — |
grid_minor (attribute) | — |
grid_radius (attribute) | — |
projection (attribute) | — |
Values
| Name | Summary / value |
|---|---|
PROVIDER_PREFERENCES | ‘User parameter:BaseApp/Preferences/Mods’ |
FreeCAD.Base
The Base value types FreeCAD exposes as a submodule.
Generated page.
tools/api_docs.pyreads this surface from the module itself. Do not edit it by hand.
FreeCAD.Base — the core data types.
Class BoundBox
| Member | Summary |
|---|---|
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
| Member | Summary |
|---|---|
DiffuseColor (attribute) | — |
Class Matrix
| Member | Summary |
|---|---|
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
| Member | Summary |
|---|---|
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.
| Member | Summary |
|---|---|
| angular() -> float | — |
| approximation() -> float | — |
| confusion() -> float | — |
| firstParameter() -> float | — |
| intersection() -> float | — |
| lastParameter() -> float | — |
| squareConfusion() -> float | — |
Class Quantity
| Member | Summary |
|---|---|
Format (attribute) | — |
Unit (attribute) | — |
UserString (attribute) | — |
Value (attribute) | — |
| getValueAs(unit) | — |
| toStr() | — |
| value_in(unit) | — |
| value_mm() | — |
Class Rotation
| Member | Summary |
|---|---|
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).
| Member | Summary |
|---|---|
NoScaling (attribute) | — |
NonUniformLeft (attribute) | — |
NonUniformRight (attribute) | — |
Other (attribute) | — |
Uniform (attribute) | — |
Class TypeId
| Member | Summary |
|---|---|
Name (attribute) | — |
| createInstance() | — |
| fromName(name) | — |
| getAllDerivedFrom(_parent) | — |
Class Unit
Class Vector
| Member | Summary |
|---|---|
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
| Member | Summary |
|---|---|
| rotate(angle) | — |
x (attribute) | — |
y (attribute) | — |
FreeCAD.Console
The console sink a workbench prints through.
Generated page.
tools/api_docs.pyreads this surface from the module itself. Do not edit it by hand.
FreeCAD.Console — minimal logging facade.
Functions
| Function | Summary |
|---|---|
PrintError(text: str = '', end: str = '\n') -> None | — |
PrintLog(text: str = '', end: str = '\n') -> None | — |
PrintMessage(text: str = '', end: str = '\n') -> None | — |
PrintStatus(*args, **kwargs) -> None | Compatibility 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.pyreads this surface from the module itself. Do not edit it by hand.
FreeCAD.Units — the units facade.
Functions
| Function | Summary |
|---|---|
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.
| Member | Summary |
|---|---|
Fixed (attribute) | — |
General (attribute) | — |
Scientific (attribute) | — |
Class Quantity
| Member | Summary |
|---|---|
Format (attribute) | — |
Unit (attribute) | — |
UserString (attribute) | — |
Value (attribute) | — |
| getValueAs(unit) | — |
| toStr() | — |
| value_in(unit) | — |
| value_mm() | — |
Class Unit
Values
| Name | Summary / value |
|---|---|
Radian | 57.29577951308232 deg |
FreeCAD
Upstream-compatible surface: documents, objects, the base value types.
Generated page.
tools/api_docs.pyreads 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
| Function | Summary |
|---|---|
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
| Member | Summary |
|---|---|
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
| Member | Summary |
|---|---|
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).
| Member | Summary |
|---|---|
| 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).
| Member | Summary |
|---|---|
NoScaling (attribute) | — |
NonUniformLeft (attribute) | — |
NonUniformRight (attribute) | — |
Other (attribute) | — |
Uniform (attribute) | — |
Values
| Name | Summary / value |
|---|---|
ActiveDocument | None |
backend | ‘ferrocad’ |
FreeCADGui
Upstream-compatible GUI surface: commands, selection and dialogs.
Generated page.
tools/api_docs.pyreads 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
| Function | Summary |
|---|---|
activeDocument() | The GuiDocument for FreeCAD.ActiveDocument, or None. |
addCommand(name: str, command) -> None | Register command under name ("<Workbench>_<Action>"). |
addDocumentObserver(observer) -> None | Upstream-shaped alias for :func:FerroCADGui.addDocumentListener. |
addModule(name: str) -> None | Record 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() -> list | The registered command ids, in registration order. |
removeCommand(name: str) -> None | Drop name from the registry (a no-op when absent). |
removeDocumentObserver(observer) -> bool | Remove 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.
| Member | Summary |
|---|---|
| close() -> None | Forget the view and its scene graph; safe to call more than once. |
Class InputHint
One status-bar hint: text plus any formatting args.
| Member | Summary |
|---|---|
args (attribute) | — |
text (attribute) | — |
Class UserInput
The small user-input vocabulary the task panels compare against.
| Member | Summary |
|---|---|
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.
| Member | Summary |
|---|---|
| addEventCallback(name, callback) -> None | Register 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) -> bool | Drop callback from name; returns whether it was registered. |
Object HintManager
The headless hint channel (FreeCADGui.HintManager).
| Member | Summary |
|---|---|
| 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.
| Member | Summary |
|---|---|
| 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.
| Member | Summary |
|---|---|
| getSelection() -> list | The selected document objects (upstream Gui.Selection). |
| getSelectionEx() -> list | [(object, [subelement names])] for the current selection. |
| isSelected(obj) -> bool | Whether obj (or an object name) is in the selection. |
Object Snapper
The module-level snapper singleton (FreeCADGui.Snapper).
| Member | Summary |
|---|---|
| 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.
| Member | Summary |
|---|---|
| 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.pyreads 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
| Function | Summary |
|---|---|
add_namespace(name: str, value) -> None | Expose value to the console under name. |
axis_summary() -> str | The axis’s provider and stem size for the shell’s popover. |
bootstrap(doc_name: str = 'Shell') -> str | Create (or recreate) the sample document and report host status. |
close_document(name: str) -> str | Close a document by name. |
commands() -> str | The registered workbench commands, with resources and live state, as JSON. |
dialog_accept(payload_json: str) -> str | Accept an open dialog: {"token": ..., "values": {...}}. |
dialog_cancel(token: str) -> str | Cancel an open dialog and drop its token. |
evaluate(code: str) -> str | Evaluate one console line, capturing stdout/stderr and any traceback. |
gpu_adapter() -> str | The persisted viewport GPU adapter setting (auto by default). |
grid_summary() -> str | The grid’s provider and spacing for the shell’s popover. |
hello() -> str | A first console line, proving the interpreter is live. |
interaction(object_name: str, manipulator: str, action: str, element: str, values_json: str) -> str | Apply a manipulator commit through the module that supports it. |
load_workbenches(dirs_json: str = '[]') -> str | Best-effort workbench loading. |
manipulator_specs(payload_json: str) -> str | The handles the mods declare for the shell’s current selection, as JSON. |
model_tree() -> str | Documents and their objects, for the inspector pane. |
open_document(path: str) -> str | Open a saved document and make it active (the --open path). |
properties(object_name: str) -> str | Property rows for one object, for the property editor pane. |
provider_features() -> str | Every registered provider feature, with its backends and active flag. |
provider_skin(payload_json: str) -> str | One feature’s provider-supplied WGSL source, for the shell to build. |
run_command(name: str) -> str | Invoke a workbench command by id; returns {ok, status?, dialog?, token?}. |
set_gpu_adapter(payload_json: str) -> str | Persist the viewport’s GPU adapter choice from the Dev Tools panel. |
set_property(object_name: str, prop: str, text: str) -> str | Set one property from its editor string, in a named transaction. |
set_provider(payload_json: str) -> str | Switch one feature’s active provider from the shell’s popover. |
set_selection(pairs_json: str) -> str | Replace FreeCADGui.Selection with the shell’s selection. |
set_view_state(state_json: str) -> str | Mirror the shell’s presentation state to FerroCADGui.ViewState. |
stage_meshes() -> str | The 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 for | The function | What comes back |
|---|---|---|
| the document tree | model_tree() | names, labels, type names |
| one object’s editors | properties(name) | rows for the property pane |
| what the viewport draws | stage_meshes() | tessellated shapes |
| the actions | commands() | ids, groups, enablement, check state |
| to run one | run_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
- The viewport reports a pick. The shell turns it into a selection snapshot and
pushes it into
FreeCADGui.Selection. - A command’s
IsActiveis evaluated against that snapshot, so the button enables or disables without anyone telling it to. - 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. - 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_acceptthen 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.
- The edit itself is one document transaction, so
undoreverses 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
docs/architecture.md— the crate graph and the call paths.docs/interaction/axis-origin-grid.md— the spatial model the grid and the navigation cube share.../how-to/toy-workbench.md— the contract above, executed.
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:
docs/workbenches— internal design notes about bringing up upstream workbenches.../python-api/— the generated signature reference.
Use this section for the contract and the patterns; use those for the internals and the exact signatures.
Who this is for
| You are | Start here |
|---|---|
| Writing a mod or workbench | minimal-mod.md, then mod-anatomy.md |
Writing a script that drives FreeCAD | commands.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:
| Mod | What it contributes |
|---|---|
mods/GridOverlay | the GridOverlay_Settings command and the ferrocad backend of the grid feature |
mods/GridOverlayCoin | the GridOverlayCoin_Settings command, the coin backend, and a Coin overlay |
python/ferrocad_toy/__init__.py | a complete small mod in one file: two commands, a display provider, an interaction provider |
The five things a mod can contribute
| Contribution | API | Registration lifetime |
|---|---|---|
| Commands | FreeCADGui.addCommand(name, command) | until removeCommand(name); a duplicate id raises |
| A display provider | FerroCAD.register_display_provider(provider) | process lifetime; there is no unregister |
| An interaction provider | FerroCAD.register_interaction_provider(provider) | process lifetime; there is no unregister |
| Feature providers | FerroCADGui.registerProvider(...) | until unregisterProvider(feature, backend); re-registering replaces the record |
| Overlays | FerroCADGui.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
minimal-mod.md— a complete mod you can load today.mod-anatomy.md— directory, scripts, lifecycle, isolation.commands.md— commands, resources, dialogs, view/tool requests.display-and-interaction.md— meshes and edits.providers.md— one backend per feature, and who owns the switch.settings-and-parameters.md—ParamGetandFerroCAD.Grid.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.
| Function | Returns |
|---|---|
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
../user/explanation/how-the-shell-talks-to-the-document.md— why the shell is a client of the facade, and why dialogs are data.../user/how-to/toy-workbench.md— a complete small mod exercised end to end.../user/how-to/selection-and-commands.md— selection, command enablement, and the dialog round trip.
What is not implemented
These are stated here so a client does not code against them:
FreeCADGui.addWorkbench,FreeCADGui.activeDocument, andViewProviderare absent. A mod is the script contract above, not an upstream workbench object.FreeCADGui.Selectionis read-only. The shell pushes the snapshot; a mod cannot add or remove selection entries.package.xmlis not read by the FerroCAD loader.- Parameters are in memory (
ParamGetis a shim); there is no cross-session persistence yet. - Overlays need a
pivy.coinbuild that exposesaddOverlay/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>".
| API | Behaviour |
|---|---|
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:
| Key | Used as | Default |
|---|---|---|
MenuText | button label | the command id |
ToolTip | hover text | "" |
Accel | shortcut string, for example "Ctrl+Shift+S" | "" |
Group | ordering label | "" (registration order) |
CmdType | metadata: "ForEdit" or "ForView" | "" |
Checkable | the row is a toggle: its popover stays open after the click so the status dot follows | False |
FerroCAD_Surface | where the action lives: grid, workplane, axis, snap, radial, mods, dock, hidden | dock |
FerroCAD_Parent | the surface’s primary command, for a row inside its popover | "" |
FerroCAD_Short | a compact label for that popover row (Grid for Snap Grid) | MenuText |
FerroCAD_Tone | a radial slot’s semantic class: structural, additive or destructive | structural |
Pixmap | accepted 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.
| Method | Absent | Raises |
|---|---|---|
IsActive | treated as True | the command is disabled and the error is reported once |
IsChecked | treated as False | treated as False and reported once |
They read:
FreeCAD.ActiveDocument— may beNone;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 returns | Shell 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
kind | Control | Value sent to the callback | Validation |
|---|---|---|---|
text | text input | str | non-empty when required |
int | text input | int | integer; min/max |
float | text input | float | float; min/max |
quantity | text input | float in canonical millimetres | unit-aware parse; min/max |
bool | on/off chip | bool | none |
enum | previous/next choice | str (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
| Action | Shell bridge | Facade (private) |
|---|---|---|
| Accept with typed values | ferrocad_shell.dialog_accept('{"token": "d1", "values": {...}}') | FreeCADGui._accept(token, values) |
| Cancel | ferrocad_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()callsFreeCADGui._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"}
| Key | Meaning |
|---|---|
grid | show or hide the reference grid |
axis_cross | show or hide the world triad |
projection | "persp"/"perspective" or "ortho"/"orthographic" |
grid_minor, grid_major | world-grid steps in millimetres; both positive, major at least minor |
grid_radius | fixed extent in millimetres; 0 is camera-derived |
grid_color | "#rgb" or "#rrggbb" |
grid_alpha | multiplier over the grid token alphas, 0..1 |
grid_overlay | hide the native ground in favour of an authored overlay; registered overlays draw either way |
workplane | an 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 theon_acceptcallback), and leave no transaction open on return. - On failure, abort the transaction and return
{"status": ...}. Activatederrors through the shell bridge are contained: the app keeps running and the status line gets the error.IsActive/IsCheckederrors disable or uncheck one command only.
Next
display-and-interaction.md— what a command’s display and interaction hooks look like.providers.md— commands that select a feature backend.docs/workbenches/actions-and-tools.md— the internal command/tool design.
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:
| Hook | Registered with | Asked when |
|---|---|---|
| display | FerroCAD.register_display_provider(provider) | the shell syncs the document’s meshes |
| interaction | FerroCAD.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.
| Field | Shape | Meaning |
|---|---|---|
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:
partsis what turns a pick intoFace6/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_partsinto a per-position part id, so a position should belong to one part. vertices/vertex_partsare topology data. The current display path records them for picking but does not draw points from them (show_verticesis off inDisplaySync).coloranddisplayare presentation only; they never affect the document.
How the shell consumes it
ferrocad_shell.stage_meshes():
- reads
FreeCAD.ActiveDocument; - skips an object that a feature consumes through its
Baselink; - calls
FerroCAD.display_mesh(obj); - adds
object(the internal name),label, andentity(the object’s 1-based position indoc.Objects); - 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}
| Argument | Meaning |
|---|---|
obj | the resolved document object |
manipulator | the manipulator registry id, for example "toy_height", "transform", "dress_up" |
action | the operation within it, for example "set", "apply", "fillet", "chamfer" |
element | the sub-element name ("Face6", "Edge3") or "" |
values | the 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):
- resolves
object_namein the active document; - parses
values_jsonas a list of numbers; - calls
FerroCAD.interaction(...); - 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
providers.md— the feature-provider registry, which is separate from these two per-object hooks.settings-and-parameters.md— where a provider’s appearance settings live.minimal-mod.md— a complete display provider.
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
| Piece | Contract |
|---|---|
AddBoxCommand.GetResources | the label, tooltip, group and ForEdit metadata the actions bar reads |
IsActive | enabled only when a document is open |
Activated | one named transaction; returns {"status": ...} |
_box_mesh | returns a mesh for an object with Length, None for everything else |
positions | 24 entries: six quads, four corners each, already placed by Placement |
triangles | 12 triangles (two per face) |
edges | 24 segments (four per face) |
triangle_parts / segment_parts | one untagged part (0), so a click selects the whole object |
color | RGBA 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
Activatedwith aquantityfield namedLength, and create the box inon_accept(commands.md). - Pick faces. Give each face a distinct
triangle_partsid and fillpartswith names such as"Face1"; a pick then reportsFace1(display-and-interaction.md). - Let the user drag it. Register an interaction provider for a manipulator
id and write the new
Length/Placementin one transaction. - Add a backend. If the mod implements a shared capability, declare it with
FerroCADGui.registerProviderinstead of keeping a private on/off flag (providers.md).
Next
mod-anatomy.md— the loader, lifecycle and isolation rules.testing-a-mod.md— turn the run above into a test.
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
| Script | Conventional role | In FerroCAD today |
|---|---|---|
Init.py | non-GUI registration: document types, display/interaction providers | executed at boot |
InitGui.py | commands, feature providers, overlays | executed 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.pyis not visible inInitGui.py. - The script is not added to
sys.modules.import MyMod.InitGuiis not the loader path. - Relative imports do not work. If a mod needs a sibling module, add its
directory to
sys.pathbefore importing it. - The loader catches exceptions per script. A failing
Init.pydoes not preventInitGui.pyfrom 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:
addCommandrejects a duplicate id withValueError. A mod that may be executed twice in one process should callFreeCADGui.removeCommand(id)first; the minimal mod andmods/GridOverlayCoindo this.registerProviderreplaces an existing(feature, backend)record, so re-registration is reload-safe.register_display_providerandregister_interaction_providerare 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
- The host configures Python, including
python/onsys.path. - The host chooses the mods directories:
HostConfig.mods_paths, elseFERROCAD_MODS_PATH, else a discoveredmods/besidepython/. ferrocad_shell.load_workbenches(dirs_json)scans each directory’s immediate subdirectories in sorted order.- For each subdirectory it executes
Init.pythenInitGui.py, if present. - 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. - 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:
| Failure | Behaviour |
|---|---|
| A script raises at load | caught by the loader; the script is recorded under errors; other scripts still run; the app boots |
GetResources() raises | treated as {}; reported once; the command keeps its id |
IsActive() raises | that command is disabled; reported once; other commands are unaffected |
IsChecked() raises | treated as False; reported once |
Activated() raises through the shell bridge | {"ok": false, "error": "..."}; the app keeps running |
A provider activate/deactivate/refresh raises | caught, reported once per (feature, backend, hook), listed under errors; the switch or refresh continues |
| A display provider raises | not caught by FerroCAD.display_mesh or stage_meshes |
| An interaction provider raises | not caught by FerroCAD.interaction or ferrocad_shell.interaction |
addOverlay fails | not 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
commands.md— what a command may return.display-and-interaction.md— the two hook signatures and the mesh fields.providers.md— feature providers and the switch.testing-a-mod.md— loading a script by path and driving it headlessly.
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:
| Mod | feature | backend | label | priority |
|---|---|---|---|---|
mods/GridOverlay | grid | ferrocad | FerroCAD (native) | 10 |
mods/GridOverlayCoin | grid | coin | Coin (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
)
| Argument | Meaning |
|---|---|
feature | non-empty string; what is implemented |
backend | non-empty string; this implementation’s id |
label | the user-facing name; falls back to backend |
command | the command id this backend owns, or None |
priority | larger wins the default; equal priority is broken by earliest registration |
activate | optional zero-argument callback: become the drawn backend |
deactivate | optional zero-argument callback: undo activate |
refresh | optional 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},
],
)
| Key | Meaning |
|---|---|
name | stable key; what setSetting addresses |
label | the row’s text |
kind | text, bool, int, float, quantity, enum, color |
default | shown until a value is set; not written at registration |
min / max | numeric range, enforced by the setter |
unit | suffix for quantity ("mm"); a unit string is accepted and converted |
choices | the closed set for enum |
get / set | optional 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:
- the persisted choice at
FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mods")→Providers/<feature>, if it names a registered backend; - otherwise the highest
priority; - 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):
- persists the choice;
- runs the outgoing backend’s
deactivate(), if any; - runs the incoming backend’s
activate(), if any; - 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/deactivateselect an implementation; they are not the only place the feature is turned on.- A provider that draws from its own store must implement
refreshso a feature-level change reaches it. - An inactive backend checks
activeProvider(feature)before drawing and inActivated, and reports why it refused.IsActivestaysTrueso 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.pywith a unique(feature, backend). - Give
Mods_Settingsa uniquelabelwithin the feature. - Check
FerroCADGui.activeProvider(feature)before drawing and inActivated; return a{"status": ...}naming the active backend when it is not you. - Make
activateidempotent and safe with no document. - Make
deactivateundo only whatactivateadded; leave user settings alone. - Implement
refreshif the feature’s master switch lives outside your store. - Keep appearance settings in your own
ParamGetgroup.
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
ParamGetshim today; there is no cross-session persistence. - Labels should be unique within a feature.
Mods_Settingsmaps 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
activeProvidercan still draw; the contract is enforced by the mods that follow it. - There is no native settings panel;
Mods_Settingsuses the ordinary dialog protocol. refreshProvidersruns only the active backend. It does not switch, persist, or callactivate.
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
| Member | Behaviour |
|---|---|
GetInt(name, default=0) / SetInt / RemInt | integer values |
GetUnsigned(name, default=0) / SetUnsigned / RemUnsigned | 32-bit unsigned; stored in the same slot as int |
GetBool(name, default=False) / SetBool / RemBool | booleans |
GetFloat(name, default=0.0) / SetFloat / RemFloat | floats |
GetString(name, default="") / SetString / RemString | strings |
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"):
| Property | Parameter | Default | Validation / meaning |
|---|---|---|---|
enabled | GridEnabled | False | show the world grid’s ground layer |
color | GridColor | "#94a3b8" | #rgb or #rrggbb; stored canonical lowercase |
opacity | GridOpacity | 1.0 | multiplier over the token alphas, 0..1 |
spacing | GridMinorStep + GridMajorStep | 10.0 / 100.0 | getter is minor; setter writes the next decade as major |
minor | GridMinorStep | 10.0 | positive, millimetres |
major | GridMajorStep | 100.0 | positive, millimetres |
radius | GridRadius | 0.0 | fixed extent in millimetres; 0 is camera-derived |
axis_cross | GridAxisCross | True | the world triad |
shadows | GridShadow | False | persisted; nothing draws it yet |
shadow_opacity | GridShadowOpacity | 0.30 | persisted; 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:
- Read in
Activatedto seed the dialog, and in the draw path if the provider draws from its own store. - Write in
on_accept, then return the{"view": ...}request that makes the shell repaint. - 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. - Validate before writing, so a rejected value leaves the store unchanged.
- 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
commands.md— how the dialog andviewrequest reach the shell.providers.md— the feature switch and therefreshcallback.../python-api/FreeCAD.md— the generated signature reference.
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 test | How |
|---|---|
| import-time registration | load InitGui.py by path, then inspect FreeCADGui.listCommands() |
| command resources and enablement | FreeCADGui._command_list(), FreeCADGui.listCommands() |
| running a command | ferrocad_shell.run_command(id) or FreeCADGui.runCommand(id) |
| dialogs, accept and cancel | ferrocad_shell.dialog_accept / dialog_cancel |
| provider selection and switching | registerProvider, activeProvider, setActiveProvider, refreshProviders |
selection-dependent IsActive | ferrocad_shell.set_selection(...) |
view-dependent IsChecked | ferrocad_shell.set_view_state(...) |
| display meshes | FerroCAD.display_mesh(obj) or ferrocad_shell.stage_meshes() |
| interaction commits | ferrocad_shell.interaction(...) |
| overlay registration | needs a pivy.coin facade; the Coin tests install a stub |
| shader compilation, drawing, the window | needs 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_selectionpayload:[{"object": name, "elements": [...]}, ...]. Objects that are no longer in the document are pruned when the snapshot is resolved.set_view_statepayload: any ofgrid,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
| Call | Returns |
|---|---|
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()insetUpbecause 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
setActiveProviderand assert the callbacks ran exactly once; unregisterProviderevery backend intearDown.
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
positionsis 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 theProviderspreference 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.coinwithaddOverlay/removeOverlay.tests/test_grid_overlay_coin.pyinstalls a fakepivy.coinand 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.ViewStateis still writable byset_view_state, andFerroCAD.Grid.snapshot()reads it only whenGuiUpis truthy.