From ab4054eb9e15efc9a46ba7f853c181ef4ffff715 Mon Sep 17 00:00:00 2001 From: forbes Date: Sat, 28 Feb 2026 14:53:51 -0600 Subject: [PATCH] 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 --- docs/ARCHITECTURE.md | 11 + docs/INTEGRATION_PLAN.md | 46 ++++ docs/src/SUMMARY.md | 2 + docs/src/development/writing-an-addon.md | 283 +++++++++++++++++++++++ docs/src/reference/kcsdk-python.md | 252 ++++++++++++++++++++ 5 files changed, 594 insertions(+) create mode 100644 docs/src/development/writing-an-addon.md create mode 100644 docs/src/reference/kcsdk-python.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 35e9c15621..c524bc3a14 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/docs/INTEGRATION_PLAN.md b/docs/INTEGRATION_PLAN.md index 5e81260862..1a5582ea81 100644 --- a/docs/INTEGRATION_PLAN.md +++ b/docs/INTEGRATION_PLAN.md @@ -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` 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. diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 29fe819bca..d129a051d2 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -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) diff --git a/docs/src/development/writing-an-addon.md b/docs/src/development/writing-an-addon.md new file mode 100644 index 0000000000..5244216892 --- /dev/null +++ b/docs/src/development/writing-an-addon.md @@ -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 `` extension block. The `` tag is required for `InitGui.py` to be loaded, even if your addon doesn't register a workbench. + +```xml + + + my-addon + My custom addon for Kindred Create. + 0.1.0 + Your Name + LGPL-2.1-or-later + + + + MyAddonWorkbench + + + + 0.1.5 + 70 + true + + sdk + + + +``` + +### 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 `sdk`** 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) diff --git a/docs/src/reference/kcsdk-python.md b/docs/src/reference/kcsdk-python.md new file mode 100644 index 0000000000..8477c921ef --- /dev/null +++ b/docs/src/reference/kcsdk-python.md @@ -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)