blender-tools/docs/blender-extension-guide.md

302 lines
9.2 KiB
Markdown

# Building Blender extensions in this repository
This guide targets Blender 5.x, with Blender 5.2 as the current user target.
Blender calls installable Python plug-ins **add-on extensions**. Use that format
instead of the legacy single-file add-on format.
## 1. Understand the package boundary
Every extension is an independent directory under `blender/`:
```text
blender/
example_tool/
blender_manifest.toml
__init__.py
operators.py # optional
properties.py # optional
ui.py # optional
wheels/ # optional bundled dependencies
```
The build command turns the contents of `example_tool/` into a ZIP whose root
contains the manifest and `__init__.py`. The directory containing all extensions
is not itself a Python package.
Start with one `__init__.py` while a tool is small. Split it when operators,
panels, and data helpers become difficult to navigate. Multi-file extensions
must use relative imports:
```python
from . import operators, properties, ui
```
Do not import the extension by a hard-coded installed name. Blender namespaces
extensions by repository, for example `bl_ext.user_default.example_tool`, and
that prefix changes when the package is installed elsewhere.
## 2. Write the manifest
A minimal repository-compatible manifest looks like this:
```toml
schema_version = "1.0.0"
id = "example_tool"
version = "0.1.0"
name = "Example Tool"
tagline = "Perform one useful Blender workflow"
maintainer = "Your Name"
type = "add-on"
blender_version_min = "5.0.0"
license = [
"SPDX:GPL-3.0-or-later",
]
```
Manifest rules worth catching early:
- `id` is the stable package identity. Do not change it merely to rename the UI.
- `version` follows semantic versioning. Bump it when distributing an update.
- `tagline` is short, has no trailing punctuation, and describes an action.
- `blender_version_min` is the oldest version actually supported, not simply the
developer's newest installed version.
- Omit unused optional fields rather than setting them to empty strings/lists.
- Use SPDX-prefixed license identifiers.
### Permissions
Declare capabilities that reach outside normal Blender data:
```toml
[permissions]
files = "Export processed textures selected by the user"
network = "Synchronize assets with the configured server"
```
Only declare what the extension uses. Permission explanations must be short and
must not end in punctuation. Network permission does not override Blender's
online-access preference; check it before connecting:
```python
if not bpy.app.online_access:
self.report({"ERROR"}, "Enable Online Access in Blender preferences")
return {"CANCELLED"}
```
## 3. Implement registration, an operator, and UI
This is a small but complete extension entry point:
```python
import bpy
from bpy.props import BoolProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup
class EXAMPLE_PG_Settings(PropertyGroup):
affect_selected: BoolProperty(
name="Affect Selected",
default=True,
)
class EXAMPLE_OT_Run(Operator):
bl_idname = "object.example_run"
bl_label = "Run Example"
bl_description = "Perform the example operation on the active mesh"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return bool(
context.mode == "OBJECT"
and context.active_object
and context.active_object.type == "MESH"
)
def execute(self, context):
obj = context.active_object
if obj is None:
self.report({"ERROR"}, "Select a mesh object")
return {"CANCELLED"}
# Prefer direct data API changes here. Use context, not bpy.context.
obj["example_was_run"] = True
self.report({"INFO"}, f"Processed {obj.name}")
return {"FINISHED"}
class EXAMPLE_PT_Panel(Panel):
bl_label = "Example Tool"
bl_idname = "EXAMPLE_PT_main"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Example"
def draw(self, context):
layout = self.layout
settings = context.scene.example_tool
layout.prop(settings, "affect_selected")
layout.operator("object.example_run")
CLASSES = (
EXAMPLE_PG_Settings,
EXAMPLE_OT_Run,
EXAMPLE_PT_Panel,
)
def register():
for cls in CLASSES:
bpy.utils.register_class(cls)
bpy.types.Scene.example_tool = PointerProperty(type=EXAMPLE_PG_Settings)
def unregister():
del bpy.types.Scene.example_tool
for cls in reversed(CLASSES):
bpy.utils.unregister_class(cls)
```
Important details:
- Register `PropertyGroup` classes before creating pointer/collection properties
that reference them.
- Unregister in exact reverse dependency order.
- Prefix class names and Blender identifiers to avoid collisions.
- A panel's `draw()` executes frequently. It should be cheap and should not
mutate scene data.
- Use `poll()` both to disable invalid UI actions and to document required context.
- `bl_options = {"REGISTER", "UNDO"}` is appropriate for scene-data changes,
but Undo does not reverse files, HTTP calls, or other external side effects.
## 4. Design safe Blender operations
Blender data often has multiple users. Editing `obj.data` can change every
object sharing that mesh, and editing a material can change every object using
it. When the feature should be non-destructive, copy first:
```python
result_object = source.copy()
result_object.data = source.data.copy()
source.users_collection[0].objects.link(result_object)
```
When modifiers must be applied without touching the source:
```python
depsgraph = context.evaluated_depsgraph_get()
evaluated = source.evaluated_get(depsgraph)
result_mesh = bpy.data.meshes.new_from_object(
evaluated,
preserve_all_data_layers=True,
depsgraph=depsgraph,
)
```
Stateful operations should follow a transaction-like structure:
```python
active_before = context.view_layer.objects.active
selected_before = list(context.selected_objects)
temporary_object = None
success = False
try:
# Create copies and perform the operation.
success = True
finally:
# Remove temporary data and restore selection/settings.
if temporary_object is not None:
bpy.data.objects.remove(temporary_object, do_unlink=True)
```
Track every temporary data-block you create. Removing an object does not
automatically remove its orphaned mesh, material, or image. Check `users == 0`
before removing a data-block that might legitimately be shared.
Use `bpy.ops` only when Blender exposes no suitable data API. Operators depend
on context: editor area, mode, active object, selected objects, active material
node, render engine, or view layer. A headless test is the quickest way to find
hidden context assumptions.
## 5. Store files and settings correctly
An installed extension may live in a read-only system repository, and upgrades
replace its package directory. Never store user data beside `__init__.py`.
Use the per-extension storage API:
```python
storage_dir = bpy.utils.extension_path_user(
__package__,
path="cache",
create=True,
)
```
Use Blender properties for settings that should live in a `.blend` file, and an
`AddonPreferences` subclass for user preferences that should apply across
projects. Access preferences through `__package__`, not a literal module name:
```python
preferences = bpy.context.preferences.addons[__package__].preferences
```
Use `bpy.path.abspath()` for Blender paths such as `//textures/output.png`.
Validate empty paths and file formats before starting an expensive operation.
## 6. Bundle dependencies
Extensions must be self-contained. For a third-party Python dependency:
1. Download/build wheels for every supported OS and architecture.
2. Put them under the extension, conventionally `wheels/`.
3. List each relative wheel path in the manifest's `wheels` array.
4. Build and test the installed ZIP on every advertised platform.
Do not import from the developer's system Python, mutate Blender's bundled
Python, or invoke `pip` during registration. Pure-Python code may be vendored,
but keep its license and avoid top-level package-name collisions.
## 7. Build and install
Build all repository extensions:
```bash
./build-all.sh
```
Build, reinstall, and enable them for the current Blender user:
```bash
./install-all.sh
```
Windows Command Prompt:
```bat
install-all.bat
```
Select a non-default Blender executable with `BLENDER_BIN`. Close running
Blender instances before reinstalling; an open process retains already imported
Python modules.
For the full validation and release sequence, continue with
[Testing and releasing](testing-and-releasing.md).
## Official references
- [Creating Blender extensions](https://docs.blender.org/manual/en/dev/advanced/extensions/getting_started.html)
- [Extension add-ons, namespaces, storage, and online access](https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html)
- [Bundling Python wheels](https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html)
- [Extension command-line arguments](https://docs.blender.org/manual/en/dev/advanced/command_line/extension_arguments.html)
- [Current Blender Python API](https://docs.blender.org/api/current/)
- [Blender extension add-on guidelines](https://developer.blender.org/docs/handbook/extensions/addon_guidelines/)