You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 040e994
Browse filesBrowse the repository at this point in the historyBrowse files
fix(skills): correct measured-false claims across eight skills and two rules (#414)
Each statement below was checked by a headless probe on 4.5.11 LTS and
5.2.1 LTS or against the API docs, and rewritten to match:
- always-free-bmesh (rule, snippet, mesh-editing skill): BMesh storage is
freed when its wrapper is collected; the reason for try/finally free()
is deterministic release and fail-fast ReferenceError, not memory the
GC can never reclaim. Edit-mode free() is an ownership rule, not a
double-free crash.
- mesh-editing: float64 into foreach_set converts correctly (~1e-7) but
is slower; objects.remove() already defaults to do_unlink=True.
- depsgraph: an evaluated object owns one temp mesh (a second to_mesh()
returns the same one); the unsourced crash claim is gone.
- headless: sys.argv is the full command line; document --factory-startup
and --python-exit-code; export_scene.obj was removed in 4.0, not blocked
by a file dialog; render.render is not special in --background.
- addon-scaffolding / bl-info-migration: a manifest alone covers 4.2+, so
bl_info matters only for 4.1 and earlier (not "4.0 or earlier").
- ai-mesh-cleanup / decimate_to_budget: Mesh.loop_triangles is lazy;
calc_loop_triangles() is optional.
- operators: REGISTER is Info + redo panel, not undo; INTERNAL hides from
search; BLOCKING blocks the cursor.
- ui-panels: a missing _PT_ only warns. procedural-materials: a second
link to Surface replaces the first. extension template README: Install
from Disk takes a zip; build it with `extension build` first.
Closes#358Closes#391
Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: claude/blender-rules.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,7 +7,7 @@ full version with Wrong/Right examples.
7
7
8
8
## always-free-bmesh
9
9
10
-
Flag bmesh.new() calls without a paired bm.free() in a try/finally block. BMesh allocates C-side memory that Python's garbage collector cannot reclaim; missing free() leaks and eventually crashes Blender.
10
+
Flag bmesh.new() calls without a paired bm.free() in a try/finally block. Blender frees an unreferenced BMesh only when its Python wrapper dies, which a traceback, global, closure or modal operator can delay for the whole session; an explicit free() in finally releases it deterministically.
11
11
12
12
Applies to: `**/*.py`. Full rule: [`rules/always-free-bmesh.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/always-free-bmesh.mdc).
Copy file name to clipboardExpand all lines: claude/skills/blender-rules/SKILL.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -12,7 +12,7 @@ full version with Wrong/Right examples.
12
12
13
13
## always-free-bmesh
14
14
15
-
Flag bmesh.new() calls without a paired bm.free() in a try/finally block. BMesh allocates C-side memory that Python's garbage collector cannot reclaim; missing free() leaks and eventually crashes Blender.
15
+
Flag bmesh.new() calls without a paired bm.free() in a try/finally block. Blender frees an unreferenced BMesh only when its Python wrapper dies, which a traceback, global, closure or modal operator can delay for the whole session; an explicit free() in finally releases it deterministically.
16
16
17
17
Applies to: `**/*.py`. Full rule: [`rules/always-free-bmesh.mdc`](https://github.com/TMHSDigital/Blender-Developer-Tools/blob/main/rules/always-free-bmesh.mdc).
Copy file name to clipboardExpand all lines: rules/always-free-bmesh.mdc
+20-12Lines changed: 20 additions & 12 deletions
Original file line number
Diff line number
Diff line change
@@ -1,5 +1,5 @@
1
1
---
2
-
description: Flag bmesh.new() calls without a paired bm.free() in a try/finally block. BMesh allocates C-side memory that Python's garbage collector cannot reclaim; missing free() leaks and eventually crashes Blender.
2
+
description: Flag bmesh.new() calls without a paired bm.free() in a try/finally block. Blender frees an unreferenced BMesh only when its Python wrapper dies, which a traceback, global, closure or modal operator can delay for the whole session; an explicit free() in finally releases it deterministically.
3
3
alwaysApply: false
4
4
globs:
5
5
- "**/*.py"
@@ -10,13 +10,21 @@ standards-version: 1.10.0
10
10
11
11
`bmesh.new()` returns a Python wrapper around a C-side BMesh structure.
12
12
The structure holds the vertex, edge, face, loop, and attribute layer
13
-
storage that the bmesh module operates on. Python's garbage collector
14
-
sees the wrapper but **cannot free the underlying C memory**. That has
15
-
to be done explicitly with `bm.free()`.
16
-
17
-
Forgetting `bm.free()` leaks. Long-running scripts and modal operators
18
-
that loop without freeing eventually exhaust memory and crash Blender,
19
-
often after the user has done significant unsaved work.
13
+
storage that the bmesh module operates on. Blender frees it when the
14
+
wrapper is garbage-collected ("typically when the script finishes
15
+
executing", per the `BMesh.free()` docs), so the problem is not that the
16
+
memory can never be reclaimed. It is **when**:
17
+
18
+
- An exception's traceback holds the frame, and with it `bm`, alive
19
+
(`sys.last_traceback` keeps the last one for the whole session).
20
+
- A global, a closure, a class attribute or a modal operator's `self`
21
+
keeps the wrapper reachable for as long as the add-on runs.
22
+
- A loop that builds a BMesh per iteration without freeing can hold many
23
+
full mesh copies at once before collection catches up.
24
+
25
+
`bm.free()` in a `finally` releases the storage at a point you choose,
26
+
on every path, and makes any later use of `bm` fail fast with
27
+
`ReferenceError` instead of quietly reading stale data.
20
28
21
29
## What this rule flags
22
30
@@ -77,8 +85,8 @@ and `bm.free()` raises. Without it, a `KeyError` reading
77
85
remaining session.
78
86
79
87
For the edit-mode case where the bmesh comes from `bmesh.from_edit_mesh()`,
80
-
**do not call `bm.free()`**. Edit-mode bmeshes are owned by Blender and
81
-
freeing them double-frees and crashes:
88
+
**do not call `bm.free()`**. Blender owns the edit-mode BMesh for as long
89
+
as the mesh is in Edit Mode; your wrapper only borrows it:
82
90
83
91
```python
84
92
# Edit-mode: Blender owns the bmesh, do not free it
@@ -124,9 +132,9 @@ RAM and survives until they restart Blender. For users on machines with
124
132
16GB or less, even modest leaks (a few hundred MB per modal session)
125
133
make Blender unusable after an hour.
126
134
127
-
The crash itself is hard to diagnose: the OOM may not happen during
135
+
The symptom is hard to diagnose: memory pressure may not surface during
128
136
your code's execution at all, but during a later modifier evaluation
Copy file name to clipboardExpand all lines: skills/addon-scaffolding/SKILL.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -199,7 +199,7 @@ def unregister():
199
199
200
200
## Compatibility paths
201
201
202
-
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.
202
+
`blender_manifest.toml` alone covers 4.2 and later, including 4.5 LTS and 5.2 LTS: with `blender_version_min = "4.5.0"`, a `bl_info`dict adds nothing. Keep `bl_info` alongside the manifest only if the add-on must also install on 4.1 or earlier, or through the legacy add-on folder / "Install legacy Add-on" path. Blender prefers the manifest when both are present.
Copy file name to clipboardExpand all lines: skills/ai-mesh-cleanup/SKILL.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -141,7 +141,7 @@ Origin at the lowest Z of the mesh (sit-on-ground) via `foreach_get` / `foreach_
141
141
142
142
### 6. Measure evaluated triangle count
143
143
144
-
`obj.data` is the authored mesh. A DECIMATE modifier does not change `obj.data`. Count triangles on the evaluated mesh, and call `calc_loop_triangles()`first. Tessellation is not implicit on 4.5 or 5.x.
144
+
`obj.data` is the authored mesh. A DECIMATE modifier does not change `obj.data`. Count triangles on the evaluated mesh. On 4.5 and 5.x `Mesh.loop_triangles` is computed lazily from the current topology, so `calc_loop_triangles()`is optional (harmless; it matters only on much older builds).
145
145
146
146
```python
147
147
defevaluated_triangle_count(obj):
@@ -176,7 +176,7 @@ Draco, selected-only, explicit `export_yup`, and `export_apply=True` so the deci
176
176
## Common AI mistakes
177
177
178
178
1.**Decimate ratio against `len(obj.data.polygons)`**. That ignores modifiers already on the stack and counts n-gons as one. Use evaluated `loop_triangles`.
179
-
2.**Skipping `calc_loop_triangles()`**. `loop_triangles` can be empty or stale. Required on 4.5 LTS and 5.x.
179
+
2.**Counting triangles on `obj.data`** instead of the evaluated mesh. A DECIMATE modifier does not change `obj.data`, so the count ignores the budget you just applied.
180
180
3.**`export_apply=True` as "apply object transforms".** It applies modifiers excluding armatures. Apply object scale first.
181
181
4.**Hulling the dense render mesh.** Over budget. Hull a coarse cage.
182
182
5.**`bm.normal_update()` for flipped faces.** Use `recalc_face_normals`.
@@ -187,7 +187,7 @@ Draco, selected-only, explicit `export_yup`, and `export_apply=True` so the deci
187
187
188
188
The cleanup sequence is the same on 4.5 LTS and 5.x:
189
189
190
-
-`Mesh.calc_loop_triangles()` is still required before `mesh.loop_triangles`on 4.5 LTS, 5.1, and 5.2. Not implicit. Verified: https://docs.blender.org/api/5.1/bpy.types.Mesh.html#bpy.types.Mesh.calc_loop_triangles and https://docs.blender.org/api/4.5/bpy.types.Mesh.html#bpy.types.Mesh.calc_loop_triangles. The 5.1 bmesh module still says tessellation "needs to be called explicitly": https://docs.blender.org/api/5.1/bmesh.html
190
+
-`Mesh.loop_triangles` is lazily computed on 4.5 LTSand 5.x: measured on 4.5.11 and 5.2.1, a fresh cube reads 12 without `calc_loop_triangles()`, as do a `to_mesh()` result and a mesh rewritten by bmesh. The "needs to be called explicitly" note in the bmesh docs is about `BMesh.calc_loop_triangles()`, the bmesh-side tessellation, not `Mesh.loop_triangles`.
191
191
-`Mesh.calc_normals()` is gone since 4.0. `bmesh.ops.recalc_face_normals` on both LTS lines.
192
192
- glTF import has no `global_scale` on either line. FBX does.
193
193
-`DecimateModifier.decimate_type='COLLAPSE'` and `ratio` are stable across 4.5 LTS and 5.x.
Copy file name to clipboardExpand all lines: skills/bl-info-migration/SKILL.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -228,7 +228,7 @@ This produces a `.zip` in `dist\` that the user installs via Edit > Preferences
228
228
|`extension build` command | Available | Available |
229
229
| Schema version |`1.0.0`|`1.0.0`|
230
230
231
-
The Extensions Platform shipped in Blender 4.2. Migrations done now will work on 4.2+ and 5.x without further changes. Code targeting 4.0 or earlier still needs the `bl_info`-only path.
231
+
The Extensions Platform shipped in Blender 4.2. Migrations done now will work on 4.2+ and 5.x without further changes. Code targeting 4.1 or earlier still needs the `bl_info` path.
Copy file name to clipboardExpand all lines: skills/depsgraph-and-evaluated-data/SKILL.md
+4-5Lines changed: 4 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -62,11 +62,10 @@ Three steps:
62
62
63
63
## The lifetime rule (critical)
64
64
65
-
Every `to_mesh()` must be paired with a `to_mesh_clear()`. If you skip the clear:
65
+
Every `to_mesh()` must be paired with a `to_mesh_clear()`. The evaluated object owns **one** temporary mesh: a second `to_mesh()` on the same object returns that same mesh (measured on 4.5.11 and 5.2.1), so a missing clear does not multiply per call. If you skip the clear:
66
66
67
-
- The temporary mesh leaks for the lifetime of the depsgraph (typically the session)
68
-
- Repeated calls accumulate memory
69
-
- In some 5.x builds the next depsgraph evaluation crashes if too many temp meshes are outstanding
67
+
- That temporary mesh stays allocated until the object is re-evaluated or freed, once per evaluated object you touched (a whole-scene exporter holds a full copy of every mesh)
68
+
- Code that keeps using the mesh after the next depsgraph update reads freed data; clearing at a known point makes the lifetime explicit
70
69
71
70
```python
72
71
obj_eval, mesh_eval = get_evaluated_mesh(obj)
@@ -180,7 +179,7 @@ When you build your own exporter on top of `evaluated_depsgraph_get()`, the deps
180
179
## Common AI mistakes
181
180
182
181
-**Reading `obj.data.vertices` and calling it "what the user sees"**. It is not. This is the single most common AI exporter bug.
183
-
-**Forgetting `to_mesh_clear`**. The temporary mesh leaks. Cumulative leaks degrade Blender's memory headroom and can crash long-running batch jobs.
182
+
-**Forgetting `to_mesh_clear`**. Each evaluated object keeps its temporary mesh until it is re-evaluated or freed. Across a whole-scene export that is a second copy of every mesh held at once.
184
183
-**Storing `mesh_eval` past the cleanup point**. Once `to_mesh_clear` runs, the mesh datablock is freed. Any references become dangling.
185
184
-**Calling `evaluated_get` without the depsgraph argument**. The signature is `obj.evaluated_get(depsgraph)`; passing nothing raises a `TypeError`.
186
185
-**Treating `obj.data` as identical to `obj_eval.data`**. They are different mesh datablocks. The first is the source; the second is post-evaluation.
`--background` (or `-b`) tells Blender not to launch a UI window. `--python` (or `-P`) runs a Python script. The script receives `sys.argv` containing everything after the `--` separator.
36
+
`--background` (or `-b`) tells Blender not to launch a UI window. `--python` (or `-P`) runs a Python script. The script's `sys.argv` is Blender's **full** command line (`blender`, `--background`, `--python`, ...); slice off everything up to and including `--` to get your own arguments, as the parser below does.
37
+
38
+
Two flags matter for unattended runs:
39
+
40
+
-`--factory-startup` ignores the user's startup file and preferences (add-ons, themes, auto-run settings), so the job behaves the same on every machine.
41
+
-`--python-exit-code N` makes an uncaught exception in the script exit Blender with code `N`. Without it, a traceback still exits **0**, and CI reports success.
37
42
38
43
## What changes without a UI
39
44
@@ -68,8 +73,8 @@ for obj in list(bpy.data.objects):
68
73
```
69
74
70
75
```python
71
-
# WRONG: requires UI for the file dialog.
72
-
bpy.ops.export_scene.obj(filepath="/tmp/out.obj") #may also fail because the OBJ exporter is now under wm.obj_export
76
+
# WRONG: export_scene.obj was the legacy Python OBJ add-on, removed in 4.0.
77
+
bpy.ops.export_scene.obj(filepath="/tmp/out.obj") #AttributeError on 4.x / 5.x
73
78
74
79
# RIGHT in 5.x: the new exporter operates on bpy.data, not the active object selection.
-`bpy.ops.render.render(write_still=True)`is one of the few `bpy.ops` calls that work in `--background`.
209
+
-`bpy.ops.render.render(write_still=True)`works in `--background`, like other operators that do not need a window or editor area.
205
210
- EEVEE's engine identifier is `BLENDER_EEVEE` on Blender 5.0+ and `BLENDER_EEVEE_NEXT` on 4.2-4.5 LTS (the id was reclaimed in 5.0 after legacy EEVEE was removed in 4.2). Detect via `bpy.app.version`.
The `try`/`finally` with `bm.free()` is **mandatory**. `bmesh.new()` allocates memory in the C side that Python's garbage collector cannot reclaim. Forgetting `bm.free()`leaks BMesh structures and eventually crashes Blender. See the rule `always-free-bmesh`.
72
+
The `try`/`finally` with `bm.free()` is **mandatory**. `bmesh.new()` allocates C-side storage that is released only when the wrapper is collected, and a traceback, global, closure or modal operator can keep it alive for the whole session. `free()`in `finally` releases it on every path and makes any later use of `bm` raise `ReferenceError`. See the rule `always-free-bmesh`.
`do_unlink=True` removes the object from any collections it was linked into before deleting it. Skip it and Blender will refuse to delete an object still referenced.
130
+
`do_unlink=True` removes the object from any collections it was linked into before deleting it. It is already the default (`remove(object, *, do_unlink=True, ...)`); passing it explicitly documents intent. With `do_unlink=False`, Blender refuses to delete an object that is still referenced.
131
131
132
132
## `foreach_set` for bulk vertex injection
133
133
@@ -264,7 +264,7 @@ After mutating selection, call `bm.select_flush_mode()` if you've changed indivi
264
264
265
265
6.**Skipping `mesh.update()`** after `foreach_set` or vertex coordinate edits. The mesh stays out of date until the next depsgraph cycle.
266
266
267
-
7.**Wrong dtype in `foreach_set`** (`float64`instead of `float32`). Silently writes garbage on some platforms.
267
+
7.**Mismatched dtype in `foreach_set`** (a `float64`buffer for a float32 attribute). The values are converted correctly (round-trip error about 1e-7, float32 precision, on 4.5.11 and 5.2.1), but slower: 38 ms vs 21 ms for 361k vertices on 5.2.1 (34 vs 24 ms on 4.5.11). Match the attribute's type (`float32` for `co`) for bulk speed.
268
268
269
269
8.**Trusting glTF as an n-gon witness.** The exporter always triangulates. A cube and a hexagon-from-dissolved-edge both ship 12 tris. Hygiene (`len(poly.vertices) <= 4`) and `Mesh.calc_tangents` (aborts on n-gons) must run on the Blender mesh. Witness: [`examples/ngon-triangulate/`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/ngon-triangulate).
Copy file name to clipboardExpand all lines: skills/operators/SKILL.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -87,10 +87,10 @@ class MESH_OT_offset_along_normals(bpy.types.Operator):
87
87
88
88
### `bl_options` you almost always want
89
89
90
-
-`'REGISTER'`: makes the operator show up in the redo panel and undo history. Without this, the operator is invisible to the user after running and properties cannot be tweaked.
90
+
-`'REGISTER'`: reports the operator in the Info editor and enables the Adjust Last Operation (redo) panel. Without it, properties cannot be tweaked after running. It does not add an undo step; that is `'UNDO'`.
91
91
-`'UNDO'`: hooks into the undo stack. Without this, your operator's effect cannot be undone with Ctrl-Z.
92
-
-`'INTERNAL'`: hides the operator from search and help. Use for operators called only by other operators or panels.
93
-
-`'BLOCKING'`: pauses other UI updates while running. Reserve for modal operators that need exclusive input.
92
+
-`'INTERNAL'`: hides the operator from operator search. Use for operators called only by other operators or panels. It still runs from Python and buttons.
93
+
-`'BLOCKING'`: blocks anything else from using the cursor while the operator runs. Reserve for modal operators that need exclusive mouse input.
94
94
-`'GRAB_CURSOR'` and `'GRAB_CURSOR_X'` / `'GRAB_CURSOR_Y'`: useful in modal operators to capture mouse motion.
0 commit comments