docs(sdk): add KCSDK API reference and addon developer guide
All checks were successful
Build and Test / build (pull_request) Successful in 29m17s
All checks were successful
Build and Test / build (pull_request) Successful in 29m17s
- docs/src/reference/kcsdk-python.md: full kcsdk Python API reference - docs/src/development/writing-an-addon.md: step-by-step addon guide - docs/INTEGRATION_PLAN.md: add Phase 7 KCSDK section - docs/ARCHITECTURE.md: add src/Gui/SDK/ to source layout - docs/src/SUMMARY.md: add new pages to mdBook navigation
This commit is contained in:
283
docs/src/development/writing-an-addon.md
Normal file
283
docs/src/development/writing-an-addon.md
Normal file
@@ -0,0 +1,283 @@
|
||||
# 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.
|
||||
|
||||
## 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
|
||||
<?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 |
|
||||
| 50-99 | Standard addons (ztools, silo) |
|
||||
| 100+ | Optional/user addons |
|
||||
|
||||
See [Package.xml Schema Extensions](./package-xml-schema.md) 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.
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```python
|
||||
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](../reference/create-module-bootstrap.md) 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.
|
||||
|
||||
```python
|
||||
# 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.
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
### Simple approach (recommended for most addons)
|
||||
|
||||
```python
|
||||
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:
|
||||
|
||||
```python
|
||||
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.
|
||||
|
||||
```python
|
||||
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:**
|
||||
```python
|
||||
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.
|
||||
|
||||
## Related
|
||||
|
||||
- [KCSDK Python API Reference](../reference/kcsdk-python.md)
|
||||
- [Package.xml Schema Extensions](./package-xml-schema.md)
|
||||
- [Create Module Bootstrap](../reference/create-module-bootstrap.md)
|
||||
Reference in New Issue
Block a user