diff --git a/rules/prefer-temp-override-over-context-copy.mdc b/rules/prefer-temp-override-over-context-copy.mdc index 329cc644..75889308 100644 --- a/rules/prefer-temp-override-over-context-copy.mdc +++ b/rules/prefer-temp-override-over-context-copy.mdc @@ -1,5 +1,5 @@ --- -description: Flag uses of `bpy.context.copy()` to override context for an operator call. The copy-and-pass pattern was deprecated in Blender 4.x and the override semantics were removed in 5.x. Use `bpy.context.temp_override(**overrides)` as a context manager instead. +description: Flag uses of `bpy.context.copy()` to override context for an operator call. Passing a context dict to an operator was deprecated in Blender 3.2 and removed in 4.0, so it already fails on the 4.5 LTS fallback. Use `bpy.context.temp_override(**overrides)` as a context manager instead. alwaysApply: false globs: - "**/*.py" @@ -9,15 +9,14 @@ standards-version: 1.10.0 # Prefer `temp_override` over `context.copy()` `bpy.context.copy()` returns a dict snapshot of the current context. In -Blender 4.x and earlier it was common to mutate that dict and pass it as +Blender 3.x and earlier it was common to mutate that dict and pass it as the first positional argument to an operator (`bpy.ops.x.y(ctx, ...)`) to run the operator under a fabricated context. That mechanism was -deprecated in 4.x and the operator override semantics were removed in -5.x. The same code on 5.x either silently ignores the override or fails -with a confusing argument-type error. +deprecated in 3.2 and removed in 4.0. The same code on 4.0 and later, +including 4.5 LTS, fails with a confusing argument-type error. The supported replacement is `bpy.context.temp_override(**overrides)`, -which works as a context manager and applies cleanly on both 4.5 LTS and +which works as a context manager and applies cleanly on 4.5 LTS and 5.x. ## What this rule flags @@ -49,8 +48,8 @@ bpy.ops.object.delete({ }) ``` -Both forms run on 4.x with deprecation warnings. On 5.x they either no-op -the override silently or raise `TypeError: Calling operator "bpy.ops.object.delete" +Both forms ran with deprecation warnings on 3.2 to 3.6. On 4.0 and later +(4.5 LTS, 5.x) they raise `TypeError: Calling operator "bpy.ops.object.delete" error, expected a string enum, not a dict`. ## Right @@ -58,14 +57,19 @@ error, expected a string enum, not a dict`. ```python import bpy -with bpy.context.temp_override(active_object=my_obj, selected_objects=[my_obj]): +with bpy.context.temp_override(active_object=my_obj, selected_editable_objects=[my_obj]): bpy.ops.object.delete() ``` `temp_override` accepts the same keyword names as the keys you would have put in the dict. It restores the previous context on exit, even -on exception. It is supported on 4.2+ and is the only override path -on 5.x. +on exception. It is supported on 3.2+ and is the only override path +on 4.0+. + +Override the key the operator actually reads. `transform_apply` and +similar operators act on `selected_editable_objects`, so +`selected_objects=[obj]` alone does not narrow them to one object; use +`selected_editable_objects=[obj]`. ## When you also need a window override @@ -84,13 +88,12 @@ with bpy.context.temp_override(window=window, area=area, region=region): ## Why it matters -The deprecated path silently drops the override on 5.x in many cases. -Code that "worked" on 4.x produces wrong results on 5.x without an -obvious failure. The user reports "my add-on broke after upgrade" and -the cause is a context override that is now ignored. +The dict-passing path was removed in 4.0, so code that "worked" on 3.x +raises on 4.5 LTS and 5.x. The user reports "my add-on broke after +upgrade" and the cause is a context override that no longer exists. `temp_override` is the canonical, supported, and forward-compatible -replacement. There is no scenario in 5.x where the dict-passing form is +replacement. There is no scenario on 4.0+ where the dict-passing form is preferable. ## Related @@ -98,5 +101,5 @@ preferable. - Skill `operators` - Snippet `temp-override-context.py` - `bpy.types.Context.temp_override`: https://docs.blender.org/api/current/bpy.types.Context.html#bpy.types.Context.temp_override -- Blender 4.0 release notes (deprecation): https://developer.blender.org/docs/release_notes/4.0/python_api/ -- Blender 5.0 release notes (removal): https://developer.blender.org/docs/release_notes/5.0/python_api/ +- Blender 3.2 release notes (deprecation): https://developer.blender.org/docs/release_notes/3.2/python_api/ +- Blender 4.0 release notes (removal): https://developer.blender.org/docs/release_notes/4.0/python_api/ diff --git a/rules/validate-imported-mesh-scale.mdc b/rules/validate-imported-mesh-scale.mdc index 3086b01b..0768d8b1 100644 --- a/rules/validate-imported-mesh-scale.mdc +++ b/rules/validate-imported-mesh-scale.mdc @@ -69,7 +69,7 @@ for obj in bpy.context.selected_objects: sx, sy, sz = obj.scale if abs(sx - 1.0) > 1e-6 or abs(sy - 1.0) > 1e-6 or abs(sz - 1.0) > 1e-6: with bpy.context.temp_override( - object=obj, active_object=obj, selected_objects=[obj] + object=obj, active_object=obj, selected_editable_objects=[obj] ): bpy.ops.object.transform_apply( location=False, rotation=True, scale=True diff --git a/skills/addon-scaffolding/SKILL.md b/skills/addon-scaffolding/SKILL.md index efced63c..6386c98e 100644 --- a/skills/addon-scaffolding/SKILL.md +++ b/skills/addon-scaffolding/SKILL.md @@ -18,7 +18,7 @@ Use this skill when the user: ## Required inputs - **Add-on name and short id** (snake_case for the manifest `id`, human-readable `name`) -- **Target Blender versions** (defaults to `blender_version_min = "4.5.0"` so the add-on works on 4.5 LTS and 5.1) +- **Target Blender versions** (defaults to `blender_version_min = "4.5.0"` so the add-on works on 4.5 LTS, 5.1 and 5.2 LTS) - **Maintainer string** (`Name ` form) - **License SPDX identifier** (`SPDX:MIT`, `SPDX:GPL-2.0-or-later`, etc.) @@ -35,12 +35,12 @@ id = "my_addon" version = "0.1.0" name = "My Add-on" tagline = "Short description, max 64 chars, no trailing period" -maintainer = "TMHSDigital " +maintainer = "Your Name " type = "add-on" blender_version_min = "4.5.0" -license = ["SPDX:MIT"] +license = ["SPDX:GPL-3.0-or-later"] copyright = ["2026 TMHSDigital"] ``` @@ -199,7 +199,7 @@ def unregister(): ## Compatibility paths -For libraries you want to be installable on both 4.5 LTS and 5.1, also keep `bl_info` for 4.5 fallback even though new submissions to the extensions platform require the manifest. Blender prefers the manifest when both are present. +For libraries you want to be installable on both 4.5 LTS and 5.2 LTS, also keep `bl_info` for 4.5 fallback even though new submissions to the extensions platform require the manifest. Blender prefers the manifest when both are present. ```python bl_info = { diff --git a/skills/bl-info-migration/SKILL.md b/skills/bl-info-migration/SKILL.md index 8cd64022..6da8ec41 100644 --- a/skills/bl-info-migration/SKILL.md +++ b/skills/bl-info-migration/SKILL.md @@ -20,7 +20,7 @@ Use this skill when the user: Yes, with caveats. -- **In 5.1 stable**: `bl_info` is supported as a fallback for legacy add-ons. The Edit > Preferences > Add-ons panel still has "Install legacy Add-on" and recognizes `bl_info` dicts. +- **In 5.2 LTS (current stable; 5.1 is prior stable)**: `bl_info` is supported as a fallback for legacy add-ons. The Edit > Preferences > Add-ons panel still has "Install legacy Add-on" and recognizes `bl_info` dicts. - **For the Extensions Platform** (extensions.blender.org and the new add-ons UI): you need a `blender_manifest.toml`. `bl_info` is ignored. - **Dual format** is officially recognized: ship a `blender_manifest.toml` and keep the `bl_info` dict. The platform reads the manifest; legacy installers read `bl_info`. Both code paths work without conditional logic. @@ -220,9 +220,9 @@ This produces a `.zip` in `dist\` that the user installs via Edit > Preferences ## Version correctness -| Topic | 4.5 LTS | 5.1 stable | +| Topic | 4.5 LTS | 5.1 / 5.2 LTS | | --- | --- | --- | -| `bl_info` recognized | Yes (primary path) | Yes (legacy fallback only) | +| `bl_info` recognized | Yes (legacy; Extensions is the primary path since 4.2) | Yes (legacy fallback only) | | `blender_manifest.toml` recognized | Yes (Extensions Platform was added in 4.2) | Yes | | Synthetic package name | `bl_ext..` | Same | | `extension build` command | Available | Available | diff --git a/skills/drivers-and-app-handlers/SKILL.md b/skills/drivers-and-app-handlers/SKILL.md index 11022edc..8dd56db4 100644 --- a/skills/drivers-and-app-handlers/SKILL.md +++ b/skills/drivers-and-app-handlers/SKILL.md @@ -269,7 +269,7 @@ The `exit_pre` handler list is new in Blender 5.1. On 4.5 LTS, fall back to OS-l ## Version correctness -| Topic | 4.5 LTS | 5.1 stable | +| Topic | 4.5 LTS | 5.1 / 5.2 LTS | | --- | --- | --- | | `exit_pre` handler | Not available | New in 5.1; use `atexit` fallback for 4.x | | `save_pre` / `save_post` signature | `(filepath)` — a string | `(filepath)` — a string (unchanged) | diff --git a/skills/engine-export-presets/SKILL.md b/skills/engine-export-presets/SKILL.md index f900fb35..497c51aa 100644 --- a/skills/engine-export-presets/SKILL.md +++ b/skills/engine-export-presets/SKILL.md @@ -32,15 +32,16 @@ Before any preset: meters in the scene (`scale_length == 1.0`), identity object ```python def apply_selected_mesh_transforms(): - for obj in list(bpy.context.selected_objects): - if obj.type != "MESH": - continue - with bpy.context.temp_override( - object=obj, active_object=obj, selected_objects=[obj] - ): - bpy.ops.object.transform_apply( - location=False, rotation=True, scale=True - ) + # One operator call for the whole selection. transform_apply reads + # selected_editable_objects, so that is the key to override; overriding + # selected_objects alone does not narrow it. + meshes = [o for o in bpy.context.selected_objects if o.type == "MESH"] + if not meshes: + return + with bpy.context.temp_override( + object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes + ): + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) ``` `use_selection=True` on every preset. Draco is opt-in on glTF; do not copy `gltf_draco_export.py` wholesale. diff --git a/skills/procedural-materials-and-shaders/SKILL.md b/skills/procedural-materials-and-shaders/SKILL.md index 5aaf7b54..2f915333 100644 --- a/skills/procedural-materials-and-shaders/SKILL.md +++ b/skills/procedural-materials-and-shaders/SKILL.md @@ -158,10 +158,10 @@ When the same subgraph appears across multiple materials, factor it into a `Shad ## Version correctness -| Topic | 4.5 LTS | 5.1 stable | Notes | +| Topic | 4.5 LTS | 5.1 / 5.2 LTS | Notes | | --- | --- | --- | --- | -| Principled BSDF | `ShaderNodeBsdfPrincipled` | Same | Some inputs renamed in 5.0; `Specular` -> `Specular IOR Level`. Use string lookup with the 5.x name. | -| Node group socket interface | `group.inputs.new` / `group.outputs.new` | `group.interface.new_socket` | Different APIs, see snippet `shader-node-group.py`. | +| Principled BSDF | `ShaderNodeBsdfPrincipled` | Same | Inputs were renamed in 4.0 with the Principled v2 rewrite: `Specular` -> `Specular IOR Level` (see the 4.0 release notes, https://developer.blender.org/docs/release_notes/4.0/). Use string lookup with the 4.0+ name. | +| Node group socket interface | `group.interface.new_socket` | `group.interface.new_socket` | Same API on every supported version. `group.inputs.new` / `outputs.new` were removed in 4.0; see snippet `shader-node-group.py`. | | EEVEE engine string | `'BLENDER_EEVEE_NEXT'` | `'BLENDER_EEVEE'` | Legacy EEVEE was removed in 4.2. EEVEE Next used the id `'BLENDER_EEVEE_NEXT'` on 4.2-4.5, then reclaimed the plain `'BLENDER_EEVEE'` id in 5.0. | | Layered Textures | Not present | Not present in 5.1 | Roadmap pushed to 2027. Do not generate code referencing it. | diff --git a/skills/vse-python/SKILL.md b/skills/vse-python/SKILL.md index 850a3d94..a876d645 100644 --- a/skills/vse-python/SKILL.md +++ b/skills/vse-python/SKILL.md @@ -38,7 +38,7 @@ else: strip.color = (0.85, 0.10, 0.22) ``` -Branch on `bpy.app.version`, never on `bpy.app.version_string`. An empty `bpy_prop_collection` is falsy — `se.strips or se.sequences` silently falls through to the legacy accessor on an empty timeline. Always branch on `hasattr`. +Branch on the `bpy.app.version` tuple for known API boundaries (such as the 5.2 COLOR strip `width`/`height` bake), never on `bpy.app.version_string`. Pick the `.strips` vs `.sequences` accessor with `hasattr`, not version: an empty `bpy_prop_collection` is falsy, so `se.strips or se.sequences` silently falls through to the legacy accessor on an empty timeline. ## Accessor: `.strips` vs `.sequences` @@ -152,6 +152,7 @@ A lone COLOR strip **does** honor `transform.scale_*` on 5.2. The break is media ## References - `bpy.types.SequenceEditor`: https://docs.blender.org/api/current/bpy.types.SequenceEditor.html -- `bpy.types.Sequence`: https://docs.blender.org/api/current/bpy.types.Sequence.html +- `bpy.types.Strip` (5.x name of `Sequence`): https://docs.blender.org/api/current/bpy.types.Strip.html +- 4.5 LTS `Sequence`: https://docs.blender.org/api/4.5/bpy.types.Sequence.html - 4.5 LTS `SequenceEditor`: https://docs.blender.org/api/4.5/bpy.types.SequenceEditor.html - 5.1 `SequenceEditor`: https://docs.blender.org/api/5.1/bpy.types.SequenceEditor.html diff --git a/snippets/action-ensure-channelbag-for-slot.py b/snippets/action-ensure-channelbag-for-slot.py index 72633b5a..b763a56c 100644 --- a/snippets/action-ensure-channelbag-for-slot.py +++ b/snippets/action-ensure-channelbag-for-slot.py @@ -9,6 +9,7 @@ # action.fcurves API is still present, so use it directly. # # Verified on Blender 4.5.10 LTS and 5.1.1. Import path (5.0+): +# from bpy_extras import anim_utils # # Reference: # https://docs.blender.org/api/current/bpy_extras.anim_utils.html diff --git a/snippets/export_preset_godot.py b/snippets/export_preset_godot.py index ae13f4c7..f624e767 100644 --- a/snippets/export_preset_godot.py +++ b/snippets/export_preset_godot.py @@ -15,15 +15,16 @@ def apply_selected_mesh_transforms(): - for obj in list(bpy.context.selected_objects): - if obj.type != "MESH": - continue - with bpy.context.temp_override( - object=obj, active_object=obj, selected_objects=[obj] - ): - bpy.ops.object.transform_apply( - location=False, rotation=True, scale=True - ) + # One operator call for the whole selection. transform_apply reads + # selected_editable_objects, so that is the key to override; overriding + # selected_objects alone does not narrow it. + meshes = [o for o in bpy.context.selected_objects if o.type == "MESH"] + if not meshes: + return + with bpy.context.temp_override( + object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes + ): + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) def export_preset_godot(filepath, selected_only=True, draco=False): diff --git a/snippets/export_preset_unity.py b/snippets/export_preset_unity.py index bc7756c5..712627b8 100644 --- a/snippets/export_preset_unity.py +++ b/snippets/export_preset_unity.py @@ -15,15 +15,16 @@ def apply_selected_mesh_transforms(): - for obj in list(bpy.context.selected_objects): - if obj.type != "MESH": - continue - with bpy.context.temp_override( - object=obj, active_object=obj, selected_objects=[obj] - ): - bpy.ops.object.transform_apply( - location=False, rotation=True, scale=True - ) + # One operator call for the whole selection. transform_apply reads + # selected_editable_objects, so that is the key to override; overriding + # selected_objects alone does not narrow it. + meshes = [o for o in bpy.context.selected_objects if o.type == "MESH"] + if not meshes: + return + with bpy.context.temp_override( + object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes + ): + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) def export_preset_unity(filepath, selected_only=True, draco=False): diff --git a/snippets/export_preset_unreal.py b/snippets/export_preset_unreal.py index be0fc883..b29855a3 100644 --- a/snippets/export_preset_unreal.py +++ b/snippets/export_preset_unreal.py @@ -1,7 +1,8 @@ # Unreal presets: centimeter scale. glTF has no global_scale and no # axis_forward / axis_up; bake 100x then export_yup=True. FBX uses # global_scale=100.0 plus axis_forward='-Z' and axis_up='Y'. That RNA -# split is the contract. glTF bake mutates selected mesh objects. +# split is the contract. glTF bake mutates selected mesh objects +# (scale and location both multiplied by 100). # Draco is glTF-only and opt-in; see snippets/gltf_draco_export.py. # # Assumption: scene units are meters before the 100x bake / FBX scale. @@ -16,22 +17,26 @@ def apply_selected_mesh_transforms(): - for obj in list(bpy.context.selected_objects): - if obj.type != "MESH": - continue - with bpy.context.temp_override( - object=obj, active_object=obj, selected_objects=[obj] - ): - bpy.ops.object.transform_apply( - location=False, rotation=True, scale=True - ) + # One operator call for the whole selection. transform_apply reads + # selected_editable_objects, so that is the key to override; overriding + # selected_objects alone does not narrow it. + meshes = [o for o in bpy.context.selected_objects if o.type == "MESH"] + if not meshes: + return + with bpy.context.temp_override( + object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes + ): + bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) def export_preset_unreal_gltf(filepath, selected_only=True, draco=False): - apply_selected_mesh_transforms() + # Scale location with scale so inter-object spacing grows with the + # geometry. Assumes unparented objects; parented ones need the parent + # chain baked first. for obj in list(bpy.context.selected_objects): if obj.type != "MESH": continue + obj.location = (obj.location[0] * 100.0, obj.location[1] * 100.0, obj.location[2] * 100.0) obj.scale = (obj.scale[0] * 100.0, obj.scale[1] * 100.0, obj.scale[2] * 100.0) apply_selected_mesh_transforms() bpy.ops.export_scene.gltf( diff --git a/snippets/shader-node-group.py b/snippets/shader-node-group.py index 019b044e..84d575bd 100644 --- a/snippets/shader-node-group.py +++ b/snippets/shader-node-group.py @@ -1,5 +1,5 @@ -# Reusable shader node group with the cross-version interface API. -# 4.5 LTS uses group.inputs/outputs; 5.x uses group.interface.new_socket. +# Reusable shader node group via NodeTree.interface (4.0+: 4.5 LTS, 5.1, 5.2). +# group.inputs/outputs.new were removed in 4.0; there is no version branch. # See skill: procedural-materials-and-shaders. # Refs: docs.blender.org/api/current/bpy.types.ShaderNodeTree.html @@ -9,14 +9,9 @@ def make_tint_group(name="Tint"): group = bpy.data.node_groups.new(name=name, type='ShaderNodeTree') - if hasattr(group, 'interface'): - group.interface.new_socket(name='Color', in_out='INPUT', socket_type='NodeSocketColor') - group.interface.new_socket(name='Strength', in_out='INPUT', socket_type='NodeSocketFloat') - group.interface.new_socket(name='Result', in_out='OUTPUT', socket_type='NodeSocketColor') - else: - group.inputs.new('NodeSocketColor', 'Color') - group.inputs.new('NodeSocketFloat', 'Strength') - group.outputs.new('NodeSocketColor', 'Result') + group.interface.new_socket(name='Color', in_out='INPUT', socket_type='NodeSocketColor') + group.interface.new_socket(name='Strength', in_out='INPUT', socket_type='NodeSocketFloat') + group.interface.new_socket(name='Result', in_out='OUTPUT', socket_type='NodeSocketColor') nodes = group.nodes links = group.links diff --git a/templates/ai-asset-pipeline-template/pipeline.py b/templates/ai-asset-pipeline-template/pipeline.py index de8d4980..386f466e 100644 --- a/templates/ai-asset-pipeline-template/pipeline.py +++ b/templates/ai-asset-pipeline-template/pipeline.py @@ -109,7 +109,7 @@ def scale_is_identity(obj, tol=1e-6): def apply_object_transform(obj): with bpy.context.temp_override( - object=obj, active_object=obj, selected_objects=[obj] + object=obj, active_object=obj, selected_editable_objects=[obj] ): bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) diff --git a/templates/extension-addon-template/blender_manifest.toml b/templates/extension-addon-template/blender_manifest.toml index 79837739..861c32e0 100644 --- a/templates/extension-addon-template/blender_manifest.toml +++ b/templates/extension-addon-template/blender_manifest.toml @@ -4,12 +4,14 @@ id = "example_addon" version = "0.1.0" name = "Example Addon" tagline = "Minimal Extensions Platform starting point" -maintainer = "TMHSDigital " +# Replace the maintainer line with your own name and a real contact address. +maintainer = "Your Name " type = "add-on" blender_version_min = "4.5.0" -license = ["SPDX:MIT"] +# extensions.blender.org requires GPL-3.0-or-later for add-ons that ship code. +license = ["SPDX:GPL-3.0-or-later"] copyright = ["2026 TMHSDigital"] website = "https://github.com/TMHSDigital/Blender-Developer-Tools"