docs(sdk): add KCSDK API reference and addon developer guide

- 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:
forbes
2026-02-28 14:53:51 -06:00
parent 5f8557fc83
commit ab4054eb9e
5 changed files with 594 additions and 0 deletions

View File

@@ -116,6 +116,17 @@ mods/silo/ [submodule → silo-mod.git] FreeCAD workbench
├── silo_commands.py Commands + FreeCADSiloSettings adapter
└── silo_origin.py FileOrigin backend for Silo (via SDK)
src/Gui/SDK/ KCSDK C++ shared library (libKCSDK.so)
├── KCSDKGlobal.h DLL export macros
├── Types.h Plain C++ types (ContextDef, DockArea, PanelPersistence)
├── IPanelProvider.h Abstract dock panel interface
├── WidgetBridge.h/.cpp PySide QWidget <-> C++ QWidget* (via Gui::PythonWrapper)
├── SDKRegistry.h/.cpp Singleton registry — contexts, panels, providers
└── bindings/ pybind11 module (kcsdk.so)
├── kcsdk_py.cpp Module definition — enums, functions, classes
├── PyIPanelProvider.h Trampoline for Python subclassing
└── PyProviderHolder.h GIL-safe forwarding wrapper
src/Gui/EditingContext.h/.cpp EditingContextResolver singleton + context registry
src/Gui/BreadcrumbToolBar.h/.cpp Color-coded breadcrumb toolbar (Catppuccin Mocha)
src/Gui/FileOrigin.h/.cpp FileOrigin base class + LocalFileOrigin

View File

@@ -167,6 +167,52 @@ Theme colors are now centralized in the SDK's YAML palette (`mods/sdk/kindred_sd
---
### Phase 7: KCSDK — C++-backed SDK module -- IN PROGRESS
**Goal:** Replace the pure-Python SDK wrappers with a C++ shared library (`libKCSDK.so`) and pybind11 bindings (`kcsdk.so`). This gives addons a stable, typed API with proper GIL safety and enables future C++ addon development without Python.
**Architecture:**
```
Python Addons (silo, future addons, ...)
|
kindred_sdk (mods/sdk/) <- convenience layer (try kcsdk, fallback FreeCADGui)
|
kcsdk.so (pybind11 module) <- C++ API bindings
|
KCSDK (C++ shared library) <- SDKRegistry + provider interfaces
|
FreeCADGui (EditingContextResolver, DockWindowManager, OriginManager, ...)
```
**Sub-phases:**
| # | Issue | Status | Description |
|---|-------|--------|-------------|
| 1 | #350 | DONE | Scaffold KCSDK library + kcsdk pybind11 module |
| 2 | #351 | DONE | Migrate editing context API to kcsdk |
| 3 | #352 | DONE | Panel provider system (IPanelProvider) |
| 4 | #353 | — | C++ theme engine |
| 5 | #354 | — | Toolbar provider system (IToolbarProvider) |
| 6 | #355 | — | Menu and action system |
| 7 | #356 | — | Status bar provider + origin migration |
| 8 | #357 | — | Deprecation cleanup + SDK v1.0.0 |
**Key files:**
- `src/Gui/SDK/` — C++ library (KCSDKGlobal.h, Types.h, SDKRegistry, IPanelProvider, WidgetBridge)
- `src/Gui/SDK/bindings/` — pybind11 module (kcsdk_py.cpp, PyIPanelProvider, PyProviderHolder)
- `mods/sdk/kindred_sdk/` — Python wrappers with kcsdk/legacy fallback
**Design decisions:**
- **No Qt in public C++ API** — `Types.h` uses `std::string`, `std::vector`, `std::function`. Qt conversion happens internally in `SDKRegistry.cpp`.
- **GIL-safe Python callables** — Python callbacks stored via `std::make_shared<py::object>` with `py::gil_scoped_acquire` before every invocation.
- **PySide widget bridging** — `WidgetBridge::toQWidget()` converts PySide QWidget objects to C++ `QWidget*` via `Gui::PythonWrapper` (Shiboken).
- **Provider pattern** — Interfaces like `IPanelProvider` enable addons to register factories. The registry calls `create_widget()` once and manages the lifecycle through `DockWindowManager`.
---
## Design decisions
1. **`Create::` namespace prefix.** All Kindred Create C++ features use this prefix to distinguish them from FreeCAD core.

View File

@@ -30,6 +30,7 @@
- [Build System](./development/build-system.md)
- [Gui Module Build](./development/gui-build-integration.md)
- [Package.xml Schema Extensions](./development/package-xml-schema.md)
- [Writing an Addon](./development/writing-an-addon.md)
# Silo Server
@@ -76,4 +77,5 @@
- [OriginSelectorWidget](./reference/cpp-origin-selector-widget.md)
- [FileOriginPython Bridge](./reference/cpp-file-origin-python.md)
- [Creating a Custom Origin (C++)](./reference/cpp-custom-origin-guide.md)
- [KCSDK Python API](./reference/kcsdk-python.md)
- [KCSolve Python API](./reference/kcsolve-python.md)

View 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)

View File

@@ -0,0 +1,252 @@
# KCSDK Python API Reference
The `kcsdk` module provides Python access to the Kindred Create addon SDK. It is built with pybind11 and installed alongside the Create module.
The `kindred_sdk` package (`mods/sdk/kindred_sdk/`) provides convenience wrappers that route through `kcsdk` when available, falling back to legacy `FreeCADGui.*` bindings. Addons should prefer `kindred_sdk` over importing `kcsdk` directly.
```python
import kcsdk # C++ bindings (low-level)
import kindred_sdk # Python wrappers (recommended)
```
## Module constants
| Name | Value | Description |
|------|-------|-------------|
| `API_VERSION_MAJOR` | `1` | KCSDK API major version |
## Enums
### DockArea
Dock widget placement area. Values match `Qt::DockWidgetArea`.
| Value | Integer | Description |
|-------|---------|-------------|
| `DockArea.Left` | 1 | Left dock area |
| `DockArea.Right` | 2 | Right dock area |
| `DockArea.Top` | 4 | Top dock area |
| `DockArea.Bottom` | 8 | Bottom dock area |
### PanelPersistence
Whether a dock panel's visibility survives application restarts.
| Value | Description |
|-------|-------------|
| `PanelPersistence.Session` | Visible until application close |
| `PanelPersistence.Persistent` | Saved to preferences and restored on next launch |
## Editing Context API
These functions manage the context-aware UI system. Contexts control which toolbars are visible based on the current editing state.
### register_context(id, label, color, toolbars, match, priority=50)
Register an editing context.
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | `str` | Unique identifier (e.g. `"myaddon.edit"`) |
| `label` | `str` | Display label template. Supports `{name}` placeholder |
| `color` | `str` | Hex color for breadcrumb (e.g. `"#f38ba8"`) |
| `toolbars` | `list[str]` | Toolbar names to show when active |
| `match` | `callable` | Zero-arg callable returning `True` when active |
| `priority` | `int` | Higher values checked first. Default 50 |
```python
kcsdk.register_context(
"myworkbench.edit",
"Editing {name}",
"#89b4fa",
["MyToolbar", "StandardViews"],
lambda: is_my_object_in_edit(),
priority=60,
)
```
### unregister_context(id)
Remove a previously registered editing context.
### register_overlay(id, toolbars, match)
Register an editing overlay. Overlays add toolbars to whatever context is currently active when `match()` returns `True`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | `str` | Unique overlay identifier |
| `toolbars` | `list[str]` | Toolbar names to append |
| `match` | `callable` | Zero-arg callable returning `True` when the overlay applies |
### unregister_overlay(id)
Remove a previously registered overlay.
### inject_commands(context_id, toolbar_name, commands)
Inject additional commands into an existing context's toolbar.
| Parameter | Type | Description |
|-----------|------|-------------|
| `context_id` | `str` | Target context identifier |
| `toolbar_name` | `str` | Toolbar within that context |
| `commands` | `list[str]` | Command names to add |
```python
kcsdk.inject_commands("partdesign.body", "PartDesign", ["MyAddon_CustomFeature"])
```
### current_context()
Return the current editing context as a dict, or `None`.
Keys: `id`, `label`, `color`, `toolbars`, `breadcrumb`, `breadcrumbColors`.
### refresh()
Force re-resolution of the editing context.
## Panel Provider API
These functions manage dock panel registration. Panels are created through the `IPanelProvider` interface and managed by `DockWindowManager`.
### IPanelProvider
Abstract base class for dock panel providers. Subclass in Python to create custom panels.
Three methods must be implemented:
```python
class MyPanel(kcsdk.IPanelProvider):
def id(self):
return "myaddon.panel"
def title(self):
return "My Panel"
def create_widget(self):
from PySide import QtWidgets
label = QtWidgets.QLabel("Hello from my addon")
return label
```
Optional methods with defaults:
| Method | Return type | Default | Description |
|--------|-------------|---------|-------------|
| `preferred_area()` | `DockArea` | `DockArea.Right` | Dock placement area |
| `persistence()` | `PanelPersistence` | `PanelPersistence.Session` | Visibility persistence |
| `context_affinity()` | `str` | `""` (always visible) | Only show in named context |
```python
class SidePanel(kcsdk.IPanelProvider):
def id(self): return "myaddon.side"
def title(self): return "Side Panel"
def create_widget(self):
from PySide import QtWidgets
return QtWidgets.QTreeWidget()
def preferred_area(self):
return kcsdk.DockArea.Left
def context_affinity(self):
return "partdesign.body" # only visible in PartDesign body context
```
### register_panel(provider)
Register a dock panel provider. The provider is stored in the registry until `create_panel()` is called to instantiate the actual dock widget.
| Parameter | Type | Description |
|-----------|------|-------------|
| `provider` | `IPanelProvider` | Panel provider instance |
### unregister_panel(id)
Remove a registered panel provider and destroy its dock widget if created.
### create_panel(id)
Instantiate the dock widget for a registered panel. Calls the provider's `create_widget()` once and embeds the result in a `QDockWidget` via `DockWindowManager`. Skips silently if the panel already exists.
### create_all_panels()
Instantiate dock widgets for all registered panels.
### registered_panels()
Return IDs of all registered panel providers as `list[str]`.
### available()
Return names of all registered providers (across all provider types) as `list[str]`.
## `kindred_sdk` Convenience Wrappers
The `kindred_sdk` Python package wraps the `kcsdk` C++ module with input validation, error handling, and fallback to legacy APIs.
### kindred_sdk.register_dock_panel(object_name, title, widget_factory, area="right", delay_ms=0)
High-level dock panel registration. Creates an anonymous `IPanelProvider` internally and schedules creation via `QTimer`.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `object_name` | `str` | | Qt object name (used as panel ID) |
| `title` | `str` | | Dock widget title |
| `widget_factory` | `callable` | | Zero-arg callable returning a `QWidget` |
| `area` | `str` | `"right"` | `"left"`, `"right"`, `"top"`, or `"bottom"` |
| `delay_ms` | `int` | `0` | Defer creation by this many milliseconds |
```python
from kindred_sdk import register_dock_panel
from PySide import QtWidgets
register_dock_panel(
"MyAddonPanel",
"My Addon",
lambda: QtWidgets.QLabel("Hello"),
area="left",
delay_ms=2000,
)
```
### Other `kindred_sdk` Wrappers
These mirror the `kcsdk` functions with added type validation and try/except error handling:
| Function | Maps to |
|----------|---------|
| `kindred_sdk.register_context()` | `kcsdk.register_context()` |
| `kindred_sdk.unregister_context()` | `kcsdk.unregister_context()` |
| `kindred_sdk.register_overlay()` | `kcsdk.register_overlay()` |
| `kindred_sdk.unregister_overlay()` | `kcsdk.unregister_overlay()` |
| `kindred_sdk.inject_commands()` | `kcsdk.inject_commands()` |
| `kindred_sdk.current_context()` | `kcsdk.current_context()` |
| `kindred_sdk.refresh_context()` | `kcsdk.refresh()` |
| `kindred_sdk.register_origin()` | `FreeCADGui.addOrigin()` |
| `kindred_sdk.unregister_origin()` | `FreeCADGui.removeOrigin()` |
| `kindred_sdk.get_theme_tokens()` | YAML palette lookup |
| `kindred_sdk.load_palette()` | `Palette` object from YAML |
| `kindred_sdk.create_version()` | Kindred Create version string |
| `kindred_sdk.freecad_version()` | FreeCAD version tuple |
## Architecture
```
Python Addon Code
|
kindred_sdk (mods/sdk/) <- convenience wrappers + validation
|
kcsdk.so (pybind11 module) <- C++ API bindings
|
libKCSDK.so (C++ shared library) <- SDKRegistry + provider interfaces
|
FreeCADGui (EditingContextResolver, DockWindowManager, OriginManager, ...)
```
When `kcsdk` is not available (console mode, build not installed), `kindred_sdk` falls back to legacy `FreeCADGui.*` Python bindings.
## Related
- [Writing an Addon](../development/writing-an-addon.md)
- [Package.xml Schema Extensions](../development/package-xml-schema.md)
- [Create Module Bootstrap](./create-module-bootstrap.md)