From cf2640783090dcbf06046f82c28c8c1d218d833f Mon Sep 17 00:00:00 2001
From: fOuttaMyPaint
Date: Sat, 13 Jun 2026 13:53:01 -0400
Subject: [PATCH] fix: correctness pass on Blender skills, snippets, and docs
Code bugs the AI serves to users:
- operators: import Vector from mathutils (not bpy.types) in the offset
example and column-normalize the basis so object scale no longer distorts
the local-axis direction.
- cross-version-property-delete: property_unset resets RNA props to default,
it does not remove a custom ID property. Use del on all versions and drop
the bogus version branch; note the property_unset distinction.
- version-branch-skeleton: ID-property deletion is not version-dependent;
clear_property now uses del unconditionally. EEVEE engine id stays as the
genuine divergence example.
- custom-properties: stop crediting property_unset for unbinding a type
PointerProperty or removing an ID property; both are del on all versions.
- shader-node-group: modernize ShaderNodeMixRGB to ShaderNodeMix with
data_type='RGBA', wiring RGBA sockets by index to avoid the node's
duplicate socket names.
Doc drift:
- README: drop the static version-0.2.0 shields badge that drifts every
release; the dynamic github/v/release badge already covers it.
- ROADMAP: v0.2.0 row -> Shipped, drop the per-row Current marker so the
action-owned Current: prose line is the single source of truth.
- SECURITY: Supported Versions -> 0.2.x; scope text -> two templates.
- CONTRIBUTING: snippet length 5 to 50 (was 5 to 30); standards-version
wording points at meta-repo STANDARDS_VERSION, not VERSION.
- CLAUDE: relabel the 7 materials/drivers/migration snippets v0.2.1 -> v0.2.0
to match ROADMAP/CHANGELOG/README.
Signed-off-by: fOuttaMyPaint
Co-authored-by: Cursor
---
CLAUDE.md | 2 +-
CONTRIBUTING.md | 14 +++++++-------
README.md | 1 -
ROADMAP.md | 2 +-
SECURITY.md | 6 +++---
skills/custom-properties/SKILL.md | 11 ++++++++---
skills/operators/SKILL.md | 6 +++++-
snippets/cross-version-property-delete.py | 17 ++++++++---------
snippets/shader-node-group.py | 13 +++++++++----
snippets/version-branch-skeleton.py | 13 ++++++-------
10 files changed, 48 insertions(+), 37 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index d4317d2..b29262a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -74,7 +74,7 @@ Small standalone `.py` files at `snippets/.py`, each 5 to 50 lines.
v0.1.0: canonical object creation and deletion, depsgraph evaluated mesh, bmesh load-edit-free, temp_override context, foreach_set vertex bulk write, register_classes_factory, PointerProperty binding, cross-version property delete, and the `action_ensure_channelbag_for_slot` slotted-actions bridge.
-v0.2.1: Principled BSDF material, driver-with-custom-function via `driver_namespace`, application handler registration, shader node group with cross-version `interface` API, `foreach_get` bulk vertex read, version-branch skeleton, and USD export with `evaluation_mode='RENDER'`.
+v0.2.0: Principled BSDF material, driver-with-custom-function via `driver_namespace`, application handler registration, shader node group with cross-version `interface` API, `foreach_get` bulk vertex read, version-branch skeleton, and USD export with `evaluation_mode='RENDER'`.
## Development Workflow
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f9a1e34..d798007 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -39,7 +39,7 @@ templates/
- **`skills/`** - one directory per skill, each containing `SKILL.md` with YAML frontmatter (`name`, `description`, `standards-version`).
- **`rules/`** - Cursor-style rules as `.mdc` files with YAML frontmatter (`description`, `alwaysApply`, `globs`, `standards-version`).
-- **`snippets/`** - small standalone `.py` files (5 to 30 lines) demonstrating a single canonical pattern.
+- **`snippets/`** - small standalone `.py` files (5 to 50 lines) demonstrating a single canonical pattern.
- **`templates/`** - copy-paste starting points; one directory per template.
## Adding a Skill
@@ -51,7 +51,7 @@ templates/
---
name: procedural-materials
description: One-line description, under 200 chars.
- standards-version:
+ standards-version:
---
```
@@ -69,7 +69,7 @@ templates/
alwaysApply: true
globs:
- "**/*.py"
- standards-version:
+ standards-version:
---
```
@@ -78,7 +78,7 @@ templates/
## Adding a Snippet
1. Add a `.py` file under `snippets/`, e.g. `snippets/depsgraph-evaluated-mesh.py`.
-2. Keep it 5 to 30 lines, fully working code, with a header comment naming the snippet and citing the relevant Blender doc URL or research section.
+2. Keep it 5 to 50 lines, fully working code, with a header comment naming the snippet and citing the relevant Blender doc URL or research section.
3. Snippets are validated for Python syntax in CI.
## Adding a Template
@@ -101,10 +101,10 @@ else:
## Standards-version Markers
-Files that participate in ecosystem drift checking must carry a `standards-version` marker matching the current meta-repo `VERSION`:
+Files that participate in ecosystem drift checking must carry a `standards-version` marker matching the current meta-repo `STANDARDS_VERSION` (which is decoupled from this repo's `VERSION`):
-- `AGENTS.md`, `CLAUDE.md`, `ROADMAP.md`: HTML comment first line, e.g. ``.
-- `skills/*/SKILL.md`, `rules/*.mdc`: YAML frontmatter field `standards-version: 1.9.1`.
+- `AGENTS.md`, `CLAUDE.md`, `ROADMAP.md`: HTML comment first line, e.g. ``.
+- `skills/*/SKILL.md`, `rules/*.mdc`: YAML frontmatter field `standards-version: `.
The drift-check workflow enforces these on every push and PR.
diff --git a/README.md b/README.md
index 1d372b7..6bb7f28 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,6 @@
-
diff --git a/ROADMAP.md b/ROADMAP.md
index a0a16d3..80076e4 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -7,7 +7,7 @@
| Version | Theme | Skills | Rules | Templates | Snippets | Status |
| --- | --- | --- | --- | --- | --- | --- |
| v0.1.0 | Foundation | 8 | 4 | 1 | 10 | Shipped |
-| v0.2.0 | Materials, drivers, migration | 12 | 6 | 2 | 17 | **Current** |
+| v0.2.0 | Materials, drivers, migration | 12 | 6 | 2 | 17 | Shipped |
| v0.3.0 | 5.2 LTS sweep, modal operators, USD | TBD | TBD | TBD | TBD | Planned |
| v1.0.0 | Stable | TBD | TBD | TBD | TBD | Planned |
diff --git a/SECURITY.md b/SECURITY.md
index 09b62d7..47b0e45 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -15,7 +15,7 @@ Please include:
## Scope
-This repository ships Markdown skill files, MDC rule files, Python snippets, and one Blender extension add-on template. The primary security concerns are:
+This repository ships Markdown skill files, MDC rule files, Python snippets, and two starter templates (a Blender extension add-on and a headless batch script). The primary security concerns are:
- **Snippets or templates demonstrating insecure patterns** (executing arbitrary code from `.blend` files, loading remote scripts without validation, leaking filesystem paths into logs).
- **The extension-addon template declaring over-broad permissions** in `blender_manifest.toml` (e.g. `network`, `files`, `clipboard`, `camera`) without a documented justification.
@@ -28,8 +28,8 @@ Issues with the Blender Python API itself (`bpy`, `bmesh`, `bpy_extras`) belong
| Version | Supported |
|---------|-----------|
-| 0.1.x | Yes |
-| < 0.1.0 | No |
+| 0.2.x | Yes |
+| < 0.2.0 | No |
## Response Timeline
diff --git a/skills/custom-properties/SKILL.md b/skills/custom-properties/SKILL.md
index f500dc7..d3fa98f 100644
--- a/skills/custom-properties/SKILL.md
+++ b/skills/custom-properties/SKILL.md
@@ -133,11 +133,11 @@ The order matters. The bound `PointerProperty` references the `PropertyGroup` cl
```python
def unregister():
- del bpy.types.Scene.my_addon # or property_unset() on 5.0+
+ del bpy.types.Scene.my_addon
bpy.utils.unregister_class(MY_ADDON_PG_settings)
```
-See the snippet `cross-version-property-delete.py` for the 5.0+ `property_unset` shape and the 4.5 LTS `del` shape.
+Unbinding a type-level property is `del bpy.types.Scene.my_addon` on all Blender versions. See the snippet `cross-version-property-delete.py` for removing a custom ID property (also `del`, version-stable).
## CollectionProperty (lists)
@@ -231,7 +231,12 @@ This is a frequent AI mistake when generating quick scripts. Always wrap state i
## Compatibility paths (4.5 LTS vs 5.0+)
-In Blender 5.0+, `bpy.types.Scene.my_addon.property_unset(...)` is the preferred unbind for individual props. In 4.5 LTS, `del bpy.types.Scene.my_addon` is still required for `PointerProperty` bindings. The snippet `cross-version-property-delete.py` shows both.
+Two distinct operations, both version-stable via `del`:
+
+- **Unbind a type-level property** (e.g. a `PointerProperty` bound to `bpy.types.Scene`): `del bpy.types.Scene.my_addon`. Same on 4.5 LTS and 5.0+.
+- **Remove a custom ID property** (the dict-style `obj["key"]`): `del obj["key"]`. Same on 4.5 LTS and 5.0+. The snippet `cross-version-property-delete.py` shows this.
+
+Do not reach for `property_unset()` here. It resets a registered RNA property to its default, which is neither unbinding a type property nor removing an ID property.
## Related
diff --git a/skills/operators/SKILL.md b/skills/operators/SKILL.md
index 81d822c..602d484 100644
--- a/skills/operators/SKILL.md
+++ b/skills/operators/SKILL.md
@@ -129,6 +129,7 @@ Pair `report({'ERROR'}, ...)` with `return {'CANCELLED'}` so the operator does n
```python
import bpy
+from mathutils import Vector
class OBJECT_OT_offset_active(bpy.types.Operator):
@@ -157,7 +158,10 @@ class OBJECT_OT_offset_active(bpy.types.Operator):
offset = (self.offset_x, self.offset_y, self.offset_z)
if self.use_local:
- obj.location += obj.matrix_world.to_3x3() @ bpy.types.Vector(offset)
+ # Column-normalize the basis so object scale does not distort the
+ # local-axis direction; to_3x3() alone carries scale into the result.
+ basis = obj.matrix_world.to_3x3().normalized()
+ obj.location += basis @ Vector(offset)
else:
obj.location.x += self.offset_x
obj.location.y += self.offset_y
diff --git a/snippets/cross-version-property-delete.py b/snippets/cross-version-property-delete.py
index 67414ee..c3d10d3 100644
--- a/snippets/cross-version-property-delete.py
+++ b/snippets/cross-version-property-delete.py
@@ -1,7 +1,10 @@
-# Cross-version property deletion: property_unset (5.0+) vs del (4.5 LTS).
-# Custom ID properties (the dict-style obj["my_key"] = value) are removed
-# differently across Blender versions. property_unset is the cleaner
-# 5.x-and-up form; del obj["key"] still works on 4.5 LTS.
+# Removing a custom ID property (the dict-style obj["my_key"] = value).
+# ID-property removal is version-stable: `del id_block[key]` works on all
+# Blender versions (4.5 LTS and 5.x alike). There is no version branch here.
+#
+# Do NOT use property_unset() for this. property_unset() RESETS a registered
+# RNA property to its default value; it does not remove a custom ID property.
+# That is a different operation.
#
# Reference:
# https://docs.blender.org/api/current/bpy.types.bpy_struct.html
@@ -13,11 +16,7 @@ def remove_custom_property(id_block, key):
if key not in id_block.keys():
return False
- if bpy.app.version >= (5, 0, 0):
- id_block.property_unset(key)
- else:
- del id_block[key]
-
+ del id_block[key]
return True
diff --git a/snippets/shader-node-group.py b/snippets/shader-node-group.py
index d1630bc..019b044 100644
--- a/snippets/shader-node-group.py
+++ b/snippets/shader-node-group.py
@@ -23,10 +23,15 @@ def make_tint_group(name="Tint"):
group_in = nodes.new('NodeGroupInput')
group_out = nodes.new('NodeGroupOutput')
- mix = nodes.new('ShaderNodeMixRGB')
+ mix = nodes.new('ShaderNodeMix')
+ mix.data_type = 'RGBA'
mix.blend_type = 'MULTIPLY'
- links.new(group_in.outputs['Color'], mix.inputs['Color1'])
- links.new(group_in.outputs['Strength'], mix.inputs['Fac'])
- links.new(mix.outputs['Color'], group_out.inputs['Result'])
+ # ShaderNodeMix shares socket names (Factor/A/B/Result) across every
+ # data_type, so name lookup is ambiguous. Wire the RGBA sockets by index:
+ # inputs[0]=Factor (Float), inputs[6]=A (Color), inputs[7]=B (Color)
+ # outputs[2]=Result (Color)
+ links.new(group_in.outputs['Strength'], mix.inputs[0])
+ links.new(group_in.outputs['Color'], mix.inputs[6])
+ links.new(mix.outputs[2], group_out.inputs['Result'])
return group
diff --git a/snippets/version-branch-skeleton.py b/snippets/version-branch-skeleton.py
index 29c1913..0af8cc3 100644
--- a/snippets/version-branch-skeleton.py
+++ b/snippets/version-branch-skeleton.py
@@ -6,18 +6,17 @@
def clear_property(obj, key):
- """Delete a custom property cross-version.
+ """Delete a custom ID property.
- Blender 5.0 introduced property_unset() as the supported path; on 4.5 LTS
- fall back to del obj[key]. Either form raises if key is absent, so check first.
+ NOT version-dependent: del obj[key] works on every Blender version
+ (4.5 LTS and 5.x). del raises if key is absent, so check membership first.
+ See get_eevee_engine_id() below for the file's genuine version-divergence
+ example.
"""
if key not in obj:
return False
- if bpy.app.version >= (5, 0, 0):
- obj.property_unset(f'["{key}"]')
- else:
- del obj[key]
+ del obj[key]
return True