Why it matters
All three engine export presets apply transforms with a per-object loop of bpy.ops.object.transform_apply under temp_override(selected_objects=[obj]). The override is meant to scope the operator to one object, but it doesn't: transform_apply acts on selected_editable_objects, which isn't overridden, so each iteration applies to the whole selection.
The final result happens to be correct only because applying is idempotent. The loop is O(N²) operator work, and it teaches a false pattern: that overriding selected_objects narrows an operator's target. It also breaks the repo's own rule rules/prefer-data-over-ops-in-loops.mdc, which none of the listed exceptions (:92-99) cover.
Evidence
snippets/export_preset_unreal.py:18-27 (same shape in export_preset_godot.py:17-26 and export_preset_unity.py):
for obj in list(bpy.context.selected_objects):
...
with bpy.context.temp_override(
object=obj, active_object=obj, selected_objects=[obj]
):
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
Reproduced headless on .scratch/5.2/blender-5.2.1-windows-x64/blender.exe (Blender 5.2.1 LTS): three selected cubes, each scale 2. The override above ran for cube a only:
PROBE1 after applying to only 'a': scales = [(1.0, 1.0, 1.0), (1.0, 1.0, 1.0), (1.0, 1.0, 1.0)]
All three had their scale applied, not just a.
Suggested approach
- Call
transform_apply once for the whole selection, outside any loop. That is what the loop effectively does already.
- If a single-object scope is really needed somewhere, override
selected_editable_objects=[obj]. Better, use the data API (obj.data.transform(matrix) plus resetting matrix_basis), which also satisfies the ops-in-loops rule.
- Make the same change in the Unity, Godot and Unreal presets, and in any skill text or template (
templates/ai-asset-pipeline-template/) that copies this pattern.
- Consider adding a line to
skills/operators or rules/prefer-temp-override-over-context-copy.mdc: overriding selected_objects does not scope operators that read selected_editable_objects.
Done when
Why it matters
All three engine export presets apply transforms with a per-object loop of
bpy.ops.object.transform_applyundertemp_override(selected_objects=[obj]). The override is meant to scope the operator to one object, but it doesn't:transform_applyacts onselected_editable_objects, which isn't overridden, so each iteration applies to the whole selection.The final result happens to be correct only because applying is idempotent. The loop is O(N²) operator work, and it teaches a false pattern: that overriding
selected_objectsnarrows an operator's target. It also breaks the repo's own rulerules/prefer-data-over-ops-in-loops.mdc, which none of the listed exceptions (:92-99) cover.Evidence
snippets/export_preset_unreal.py:18-27(same shape inexport_preset_godot.py:17-26andexport_preset_unity.py):Reproduced headless on
.scratch/5.2/blender-5.2.1-windows-x64/blender.exe(Blender 5.2.1 LTS): three selected cubes, each scale 2. The override above ran for cubeaonly:All three had their scale applied, not just
a.Suggested approach
transform_applyonce for the whole selection, outside any loop. That is what the loop effectively does already.selected_editable_objects=[obj]. Better, use the data API (obj.data.transform(matrix)plus resettingmatrix_basis), which also satisfies the ops-in-loops rule.templates/ai-asset-pipeline-template/) that copies this pattern.skills/operatorsorrules/prefer-temp-override-over-context-copy.mdc: overridingselected_objectsdoes not scope operators that readselected_editable_objects.Done when
bpy.opsinside a per-object loop.