Files
create/docs/src/development/writing-an-addon.md
forbes-0023 2f89f8cbb0
All checks were successful
Build and Test / build (pull_request) Successful in 30m3s
docs: add example addon template (#395)
- Create docs/examples/example-addon/ with a complete, copy-paste-ready
  addon skeleton demonstrating command registration, context injection,
  dock panels, lifecycle hooks, and event bus subscription
- Add Examples section to docs/src/SUMMARY.md
- Add quick-start cross-reference in writing-an-addon.md
2026-03-05 10:32:12 -06:00

8.2 KiB

Writing an Addon

This guide walks through creating a Kindred Create addon from scratch. Addons are Python packages in the mods/ directory that extend Create with commands, panels, and UI modifications through the SDK.

Quick start: A complete working example is available at docs/examples/example-addon/. Copy the directory into mods/ and rename it to start building.

Addon structure

A minimal addon has this layout:

mods/my-addon/
├── package.xml         # Manifest (required)
├── Init.py             # Console-phase bootstrap
├── InitGui.py          # GUI-phase bootstrap
└── my_addon/
    ├── __init__.py
    └── commands.py     # Your commands

Step 1: Create the manifest

Every addon needs a package.xml with a <kindred> extension block. The <workbench> tag is required for InitGui.py to be loaded, even if your addon doesn't register a workbench.

<?xml version="1.0" encoding="UTF-8"?>
<package format="1">
    <name>my-addon</name>
    <description>My custom addon for Kindred Create.</description>
    <version>0.1.0</version>
    <maintainer email="you@example.com">Your Name</maintainer>
    <license>LGPL-2.1-or-later</license>

    <!-- Required for InitGui.py loading -->
    <workbench>
        <classname>MyAddonWorkbench</classname>
    </workbench>

    <kindred>
        <min_create_version>0.1.5</min_create_version>
        <load_priority>70</load_priority>
        <pure_python>true</pure_python>
        <dependencies>
            <dependency>sdk</dependency>
        </dependencies>
    </kindred>
</package>

Priority ranges

Range Use
0-9 SDK and core infrastructure
10-49 Foundation addons (solver, gears, datums)
50-99 Standard addons (silo)
100+ Optional/user addons

The loader validates manifests at parse time: load_priority must be a valid integer, version strings must be dotted-numeric (e.g. 0.1.5), context IDs must be alphanumeric with dots/underscores, and dependency names are cross-checked against all discovered addons. All errors are accumulated and reported together.

See Package.xml Schema Extensions for the full schema.

Step 2: Console bootstrap (Init.py)

Init.py runs during FreeCAD's console initialization, before the GUI exists. Use it for non-GUI setup.

import FreeCAD

FreeCAD.Console.PrintLog("my-addon: loaded (console)\n")

Step 3: GUI bootstrap (InitGui.py)

InitGui.py runs when the GUI is ready. This is where you register commands, contexts, panels, and overlays.

import FreeCAD
import FreeCADGui

FreeCAD.Console.PrintLog("my-addon: loaded (GUI)\n")


def _deferred_setup():
    """Register commands and UI after the main window is ready."""
    from my_addon import commands
    commands.register()


from PySide.QtCore import QTimer
QTimer.singleShot(2000, _deferred_setup)

Deferred setup via QTimer.singleShot() avoids timing issues during startup. See Create Module Bootstrap for the full timer cascade.

Step 4: Register commands

FreeCAD commands use Gui.addCommand(). This is a stable FreeCAD API and does not need SDK wrappers.

# my_addon/commands.py

import FreeCAD
import FreeCADGui


class MyCommand:
    def GetResources(self):
        return {
            "MenuText": "My Command",
            "ToolTip": "Does something useful",
        }

    def Activated(self):
        FreeCAD.Console.PrintMessage("My command activated\n")

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


def register():
    FreeCADGui.addCommand("MyAddon_MyCommand", MyCommand())

Step 5: Inject into editing contexts

Use the SDK to add your commands to existing toolbar contexts, rather than creating a standalone workbench.

from kindred_sdk import inject_commands

# Add your command to the PartDesign body context toolbar
inject_commands("partdesign.body", "PartDesign", ["MyAddon_MyCommand"])

Built-in contexts you can inject into: sketcher.edit, assembly.edit, partdesign.feature, partdesign.body, assembly.idle, spreadsheet, empty_document, no_document.

Step 6: Register a custom context

If your addon has its own editing mode, register a context to control which toolbars are visible.

from kindred_sdk import register_context

def _is_my_object_in_edit():
    import FreeCADGui
    doc = FreeCADGui.activeDocument()
    if doc and doc.getInEdit():
        obj = doc.getInEdit().Object
        return obj.isDerivedFrom("App::FeaturePython") and hasattr(obj, "MyAddonType")
    return False

register_context(
    "myaddon.edit",
    "Editing {name}",
    "#f9e2af",                        # Catppuccin yellow
    ["MyAddonToolbar", "StandardViews"],
    _is_my_object_in_edit,
    priority=55,
)

Step 7: Register a dock panel

For panels that live in the dock area (like Silo's database panels), use the SDK panel registration.

from kindred_sdk import register_dock_panel

def _create_my_panel():
    from PySide import QtWidgets
    widget = QtWidgets.QTreeWidget()
    widget.setHeaderLabels(["Name", "Value"])
    return widget

register_dock_panel(
    "MyAddonPanel",       # unique object name
    "My Addon",           # title bar text
    _create_my_panel,
    area="right",
    delay_ms=3000,        # create 3 seconds after startup
)

Advanced approach (IPanelProvider)

For full control over panel behavior, implement the IPanelProvider interface directly:

import kcsdk

class MyPanelProvider(kcsdk.IPanelProvider):
    def id(self):
        return "myaddon.inspector"

    def title(self):
        return "Inspector"

    def create_widget(self):
        from PySide import QtWidgets
        tree = QtWidgets.QTreeWidget()
        tree.setHeaderLabels(["Property", "Value"])
        return tree

    def preferred_area(self):
        return kcsdk.DockArea.Left

    def context_affinity(self):
        return "myaddon.edit"  # only visible in your custom context

# Register and create
kcsdk.register_panel(MyPanelProvider())
kcsdk.create_panel("myaddon.inspector")

Step 8: Use theme colors

The SDK provides the Catppuccin Mocha palette for consistent theming.

from kindred_sdk import get_theme_tokens, load_palette

# Quick lookup
tokens = get_theme_tokens()
blue = tokens["blue"]     # "#89b4fa"
error = tokens["error"]   # mapped from semantic role

# Full palette object
palette = load_palette()
palette.get("accent.primary")   # semantic role lookup
palette.get("mauve")            # direct color lookup

# Format QSS templates
qss = palette.format_qss("background: {base}; color: {text};")

Complete example

Putting it all together, here's a minimal addon that adds a command and a dock panel:

mods/my-addon/
├── package.xml
├── Init.py
├── InitGui.py
└── my_addon/
    ├── __init__.py
    └── commands.py

InitGui.py:

import FreeCAD

def _setup():
    from my_addon.commands import register
    from kindred_sdk import inject_commands, register_dock_panel

    register()
    inject_commands("partdesign.body", "PartDesign", ["MyAddon_MyCommand"])

    from PySide import QtWidgets
    register_dock_panel(
        "MyAddonPanel", "My Addon",
        lambda: QtWidgets.QLabel("Hello from my addon"),
        area="right", delay_ms=0,
    )

from PySide.QtCore import QTimer
QTimer.singleShot(2500, _setup)

Key patterns

  • Use kindred_sdk wrappers instead of FreeCADGui.* internals. The SDK handles fallback and error logging.
  • Defer initialization with QTimer.singleShot() to avoid startup timing issues.
  • Declare <dependency>sdk</dependency> in your manifest to ensure the SDK loads before your addon.
  • Inject commands into existing contexts rather than creating standalone workbenches. This gives users a unified toolbar experience.
  • Use theme tokens from the palette for colors. Don't hardcode hex values.