Skip to content

Commit 3b0f5a0

Browse files
TMHSDigitalclaude
andcommitted
fix(content): port the #297/#298 export contract to template, skill, rule, example
PR #309 fixed the snippets only. The same two defects remained elsewhere: - templates/ai-asset-pipeline-template/pipeline.py: the Unreal path scaled obj.scale x100 but not obj.location, so node translations stayed in meters under centimeter geometry (origin_to_base always moves location.z, so every LOD was affected). apply_selected_mesh_transforms looped transform_apply per object. Both now match snippets/export_preset_unreal.py / _unity.py. - skills/ai-mesh-cleanup/SKILL.md: apply_object_transform overrode selected_objects=[obj] (does not narrow transform_apply) and was called in a loop. Now apply_transforms(objs): one call, selected_editable_objects. - rules/validate-imported-mesh-scale.mdc: the "required pattern" looped transform_apply per object, against prefer-data-over-ops-in-loops. Now one call over the scaled meshes. - examples/export-preset-axis: per-object loop with selected_objects=[obj] replaced by the snippet body. Proof (local, .scratch/blender-5.2.1-windows-x64/blender.exe, 5.2.1 LTS): - pipeline --preset unreal on tests/smoke/make_pipeline_glb.py fixture: lod0 node translation before [0, -1, 0] (m), after [0, -100, 0] (cm), matching the cm POSITION data. - export-preset-axis: happy path exit 0 ("export-preset-axis OK"); falsifier --same-axis still exits 9. - check_import_export_rules, check_exit_code_readme, build_claude_rules --check pass. Closes #326 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7afd8a3 commit 3b0f5a0

4 files changed

Lines changed: 50 additions & 32 deletions

File tree

‎examples/export-preset-axis/export_preset_axis.py‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -303,15 +303,16 @@ def principled(name, color, metallic, roughness):
303303

304304

305305
def apply_selected_mesh_transforms():
306-
for obj in list(bpy.context.selected_objects):
307-
if obj.type != "MESH":
308-
continue
309-
with bpy.context.temp_override(
310-
object=obj, active_object=obj, selected_objects=[obj]
311-
):
312-
bpy.ops.object.transform_apply(
313-
location=False, rotation=True, scale=True
314-
)
306+
# Same body as snippets/export_preset_unity.py: one operator call for the
307+
# whole selection. transform_apply acts on selected_editable_objects, so
308+
# that is the key to override; selected_objects alone does not narrow it.
309+
meshes = [o for o in bpy.context.selected_objects if o.type == "MESH"]
310+
if not meshes:
311+
return
312+
with bpy.context.temp_override(
313+
object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes
314+
):
315+
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
315316

316317

317318
def export_selected(path, kwargs):

‎rules/validate-imported-mesh-scale.mdc‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,18 @@ if scene.unit_settings.system not in {"METRIC", "NONE"}:
6363
scene.unit_settings.system = "METRIC"
6464
scene.unit_settings.scale_length = 1.0
6565

66-
for obj in bpy.context.selected_objects:
67-
if obj.type != "MESH":
68-
continue
69-
sx, sy, sz = obj.scale
70-
if abs(sx - 1.0) > 1e-6 or abs(sy - 1.0) > 1e-6 or abs(sz - 1.0) > 1e-6:
71-
with bpy.context.temp_override(
72-
object=obj, active_object=obj, selected_editable_objects=[obj]
73-
):
74-
bpy.ops.object.transform_apply(
75-
location=False, rotation=True, scale=True
76-
)
66+
def scaled(obj):
67+
return any(abs(s - 1.0) > 1e-6 for s in obj.scale)
68+
69+
# One transform_apply for every scaled mesh, not one call per object.
70+
# transform_apply acts on selected_editable_objects, so that is the key to
71+
# override; selected_objects alone does not narrow it.
72+
meshes = [o for o in bpy.context.selected_objects if o.type == "MESH" and scaled(o)]
73+
if meshes:
74+
with bpy.context.temp_override(
75+
object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes
76+
):
77+
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
7778
```
7879

7980
For FBX, pass `global_scale=1.0` on import as well, then still check

‎skills/ai-mesh-cleanup/SKILL.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,16 @@ def scale_is_identity(obj, tol=1e-6):
4646
return abs(sx - 1.0) < tol and abs(sy - 1.0) < tol and abs(sz - 1.0) < tol
4747

4848

49-
def apply_object_transform(obj):
49+
def apply_transforms(objs):
50+
# One operator call for the whole list. transform_apply acts on
51+
# selected_editable_objects, so that is the key to override; overriding
52+
# selected_objects alone does not narrow it.
53+
if not objs:
54+
return
5055
with bpy.context.temp_override(
51-
object=obj,
52-
active_object=obj,
53-
selected_objects=[obj],
56+
object=objs[0],
57+
active_object=objs[0],
58+
selected_editable_objects=list(objs),
5459
):
5560
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
5661

@@ -107,9 +112,7 @@ if not scene_units_are_meters(scene):
107112
scene.unit_settings.system = "METRIC"
108113
scene.unit_settings.scale_length = 1.0
109114

110-
for obj in imported_meshes():
111-
if not scale_is_identity(obj):
112-
apply_object_transform(obj)
115+
apply_transforms([o for o in imported_meshes() if not scale_is_identity(o)])
113116
```
114117

115118
`export_apply=True` on glTF applies **modifiers**, not object scale. Unapplied object scale lands on the glTF node. Witness: `examples/unapplied-scale-gltf/`.

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,16 @@ def box_collider(obj, name=None):
236236

237237

238238
def apply_selected_mesh_transforms():
239-
# Duplicated from snippets/export_preset_unity.py
240-
for obj in list(bpy.context.selected_objects):
241-
if obj.type != "MESH":
242-
continue
243-
apply_object_transform(obj)
239+
# Duplicated from snippets/export_preset_unity.py. One operator call for
240+
# the whole selection; transform_apply reads selected_editable_objects,
241+
# so that is the key to override (selected_objects alone does not narrow it).
242+
meshes = [o for o in bpy.context.selected_objects if o.type == "MESH"]
243+
if not meshes:
244+
return
245+
with bpy.context.temp_override(
246+
object=meshes[0], active_object=meshes[0], selected_editable_objects=meshes
247+
):
248+
bpy.ops.object.transform_apply(location=False, rotation=True, scale=True)
244249

245250

246251
def select_only(obj):
@@ -272,9 +277,17 @@ def export_preset(filepath, preset, draco):
272277
export_animations=False,
273278
)
274279
return
280+
# Unreal: centimeters. Scale location with scale (as in
281+
# snippets/export_preset_unreal.py) so node translations are in cm too;
282+
# origin_to_base() leaves location.z off zero, so this always matters.
275283
for obj in list(bpy.context.selected_objects):
276284
if obj.type != "MESH":
277285
continue
286+
obj.location = (
287+
obj.location[0] * 100.0,
288+
obj.location[1] * 100.0,
289+
obj.location[2] * 100.0,
290+
)
278291
obj.scale = (
279292
obj.scale[0] * 100.0,
280293
obj.scale[1] * 100.0,

0 commit comments

Comments
 (0)