Skip to content

Commit ae57f0d

Browse files
TMHSDigitalclaude
andauthored
fix(skills): correct four API claims that make agent code silently wrong (#410)
- use-foreach-set rule listed vertex "normal" and "index" as foreach_set targets; both are read-only and foreach_set on them is a silent no-op. It now names writable attributes and says how to read and write normals. - geometry-nodes-python told agents to list node types with GeometryNode.__subclasses__(), which returns [] because bpy.types classes are created lazily. Use dir(bpy.types) instead. - engine-export-presets said glTF infers the format from the filepath suffix. It does not: a .gltf path with no export_format writes .glb. The skill now says to always pass export_format, and every glTF call in the snippets, presets and pipeline template passes it. - drivers-and-app-handlers' save-count example keyed an ID property by the full path (KeyError past 63 characters) and counted in save_post, after the file was written. It now keys by basename and uses save_pre. Closes #353 Closes #355 Closes #388 Closes #390 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e5f7938 commit ae57f0d

9 files changed

Lines changed: 30 additions & 14 deletions

File tree

‎rules/use-foreach-set-for-bulk-data.mdc‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,12 @@ Same 100,000 vertices: under a millisecond.
6666

6767
The signature is `foreach_set(attribute, sequence)` where:
6868

69-
- `attribute` is the name of the per-element attribute as a string
70-
(`"co"`, `"normal"`, `"index"`, `"use_smooth"`, etc.).
69+
- `attribute` is the name of a **writable** per-element attribute as a
70+
string (`"co"`, `"use_smooth"`, `"material_index"`, etc.). Read-only
71+
attributes such as vertex `"normal"` and `"index"` accept `foreach_set`
72+
without error and change nothing (measured on 5.2.1 LTS): vertex normals
73+
are derived, so read them with `foreach_get` (or `mesh.vertex_normals`)
74+
and write custom normals with `mesh.normals_split_custom_set(...)`.
7175
- `sequence` is a flat 1D iterable of `len(elements) * components`
7276
values. For vector attributes (3 floats per vertex), the buffer is
7377
3x as long as the element count.
@@ -85,8 +89,6 @@ import numpy as np
8589

8690
n = len(mesh.vertices)
8791
coords = np.empty(n * 3, dtype=np.float32)
88-
mesh.vertices.foreach_get("co", coords)
89-
9092
mesh.vertices.foreach_get("co", coords)
9193
xs = coords[0::3]
9294
ys = coords[1::3]

‎skills/drivers-and-app-handlers/SKILL.md‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,21 +210,24 @@ from bpy.app.handlers import persistent
210210

211211
@persistent
212212
def increment_save_count(filepath):
213-
# save_post passes the saved file path (a string) as its only argument.
214-
# Store the running count on the current scene.
213+
# save_pre runs before the write, so the new count lands in the file being
214+
# saved (a save_post write would only reach the next save). It receives the
215+
# target path. ID-property keys are limited to 63 characters, so key by the
216+
# file name, never the full path, which raises KeyError when longer.
215217
scene = bpy.context.scene
218+
key = bpy.path.basename(filepath)[:63] or "untitled"
216219
counts = scene.get('save_counts', {})
217-
counts[filepath] = counts.get(filepath, 0) + 1
220+
counts[key] = counts.get(key, 0) + 1
218221
scene['save_counts'] = counts
219222

220223

221224
def register():
222-
bpy.app.handlers.save_post.append(increment_save_count)
225+
bpy.app.handlers.save_pre.append(increment_save_count)
223226

224227

225228
def unregister():
226-
if increment_save_count in bpy.app.handlers.save_post:
227-
bpy.app.handlers.save_post.remove(increment_save_count)
229+
if increment_save_count in bpy.app.handlers.save_pre:
230+
bpy.app.handlers.save_pre.remove(increment_save_count)
228231
```
229232

230233
### Worked example: cleanup on exit (5.1+)

‎skills/engine-export-presets/SKILL.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def apply_selected_mesh_transforms():
5151
```python
5252
bpy.ops.export_scene.gltf(
5353
filepath=path,
54+
export_format="GLB",
5455
use_selection=True,
5556
export_yup=True,
5657
export_apply=True,
@@ -68,6 +69,7 @@ Snippet: [`snippets/export_preset_unity.py`](https://github.com/TMHSDigital/Blen
6869
```python
6970
bpy.ops.export_scene.gltf(
7071
filepath=path,
72+
export_format="GLB",
7173
use_selection=True,
7274
export_yup=False,
7375
export_apply=True,
@@ -115,7 +117,7 @@ Snippet: [`snippets/export_preset_unreal.py`](https://github.com/TMHSDigital/Ble
115117

116118
Probed on 4.5 LTS, 5.1, and 5.2: `export_yup` on glTF and `axis_forward` / `axis_up` / `global_scale` on FBX are present on all three. No version branch for the axis kwargs. Guard by requiring those names in operator RNA so a future rename fails loudly, as [`examples/gltf-export-roundtrip/`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gltf-export-roundtrip) does.
117119

118-
`export_format` defaults differ by call site; pass the filepath suffix (`.glb` / `.gltf` / `.fbx`) and let the operator infer, or set `export_format` explicitly on glTF.
120+
Always pass `export_format` on glTF and make the filepath suffix match it. The operator does **not** infer the format from the suffix: `export_scene.gltf(filepath="x.gltf")` with no `export_format` writes a binary `x.glb`, so the requested path never exists (measured on 4.5.11 and 5.2.1). Use `export_format="GLB"` with `.glb`, or `"GLTF_SEPARATE"` with `.gltf`.
119121

120122
## Related
121123

‎skills/geometry-nodes-python/SKILL.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,10 @@ To list all available Geometry node types in your Blender version:
164164

165165
```python
166166
import bpy
167-
for cls in bpy.types.GeometryNode.__subclasses__():
168-
print(cls.bl_idname)
167+
# bpy.types registers node classes lazily, so GeometryNode.__subclasses__()
168+
# returns [] until something touches them. dir(bpy.types) lists them all.
169+
for name in sorted(n for n in dir(bpy.types) if n.startswith("GeometryNode")):
170+
print(name)
169171
```
170172

171173
## Wiring nodes
@@ -323,7 +325,7 @@ def has_for_each_element():
323325
tree.nodes.new('GeometryNodeSetPosition') # RIGHT
324326
```
325327

326-
When in doubt, list `bpy.types.GeometryNode.__subclasses__()` and grep.
328+
When in doubt, list `[n for n in dir(bpy.types) if n.startswith("GeometryNode")]` and grep. (`bpy.types.GeometryNode.__subclasses__()` returns an empty list: the classes are created lazily.)
327329

328330
3. **Linking nodes by name when both inputs share a name** (`Value`, `Vector`, `Geometry`) and getting the wrong one. Use indices or the named inputs of the parent node.
329331

‎snippets/export_preset_godot.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def export_preset_godot(filepath, selected_only=True, draco=False):
3131
apply_selected_mesh_transforms()
3232
bpy.ops.export_scene.gltf(
3333
filepath=filepath,
34+
export_format="GLB",
3435
use_selection=selected_only,
3536
export_yup=False,
3637
export_apply=True,

‎snippets/export_preset_unity.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def export_preset_unity(filepath, selected_only=True, draco=False):
3131
apply_selected_mesh_transforms()
3232
bpy.ops.export_scene.gltf(
3333
filepath=filepath,
34+
export_format="GLB",
3435
use_selection=selected_only,
3536
export_yup=True,
3637
export_apply=True,

‎snippets/export_preset_unreal.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def export_preset_unreal_gltf(filepath, selected_only=True, draco=False):
4141
apply_selected_mesh_transforms()
4242
bpy.ops.export_scene.gltf(
4343
filepath=filepath,
44+
export_format="GLB",
4445
use_selection=selected_only,
4546
export_yup=True,
4647
export_apply=True,

‎snippets/gltf_draco_export.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
def export_gltf_draco(filepath, selected_only=True, yup=True):
1515
bpy.ops.export_scene.gltf(
1616
filepath=filepath,
17+
export_format="GLB",
1718
use_selection=selected_only,
1819
export_draco_mesh_compression_enable=True,
1920
export_draco_mesh_compression_level=6,

‎templates/ai-asset-pipeline-template/pipeline.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ def export_preset(filepath, preset, draco):
271271
if preset == "unity":
272272
bpy.ops.export_scene.gltf(
273273
filepath=filepath,
274+
export_format="GLB",
274275
use_selection=True,
275276
export_yup=True,
276277
export_apply=True,
@@ -281,6 +282,7 @@ def export_preset(filepath, preset, draco):
281282
if preset == "godot":
282283
bpy.ops.export_scene.gltf(
283284
filepath=filepath,
285+
export_format="GLB",
284286
use_selection=True,
285287
export_yup=False,
286288
export_apply=True,
@@ -307,6 +309,7 @@ def export_preset(filepath, preset, draco):
307309
apply_selected_mesh_transforms()
308310
bpy.ops.export_scene.gltf(
309311
filepath=filepath,
312+
export_format="GLB",
310313
use_selection=True,
311314
export_yup=True,
312315
export_apply=True,

0 commit comments

Comments
 (0)