Skip to content

Commit 4cdaa69

Browse files
feat: flag mixed glTF/FBX axis RNA in the import/export harness
export_scene.gltf does not take axis_forward/axis_up; export_scene.fbx does not take export_yup. The check is per-call so a file that correctly uses both exporters still passes. Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent bf8c26c commit 4cdaa69

2 files changed

Lines changed: 128 additions & 2 deletions

File tree

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
description: Flag export_scene.gltf calls that pass FBX axis_forward or axis_up, and export_scene.fbx calls that pass glTF export_yup. The two exporters do not share axis RNA.
3+
alwaysApply: true
4+
globs:
5+
- "**/*.py"
6+
standards-version: 1.10.0
7+
---
8+
9+
# Use correct axis RNA per exporter
10+
11+
`bpy.ops.export_scene.gltf` exposes axis as `export_yup` (bool).
12+
`bpy.ops.export_scene.fbx` exposes `axis_forward` and `axis_up` (axis
13+
enums) plus `global_scale`. Mixing them is the usual engine-preset bug:
14+
the glTF call raises or ignores FBX names, and the FBX call never sees
15+
`export_yup`.
16+
17+
Verified: https://docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.gltf
18+
and https://docs.blender.org/api/current/bpy.ops.export_scene.html#bpy.ops.export_scene.fbx
19+
20+
## What this rule flags
21+
22+
A `bpy.ops.export_scene.gltf(...)` call whose arguments include
23+
`axis_forward` or `axis_up`. A `bpy.ops.export_scene.fbx(...)` call whose
24+
arguments include `export_yup`. Whole-file scans are not enough: a file
25+
that correctly calls both exporters (Unreal glTF plus Unreal FBX) must
26+
still pass.
27+
28+
```python
29+
# WRONG: FBX axis names on the glTF operator
30+
bpy.ops.export_scene.gltf(
31+
filepath=path,
32+
use_selection=True,
33+
axis_forward="-Z",
34+
axis_up="Y",
35+
)
36+
```
37+
38+
```python
39+
# WRONG: glTF Y-up flag on the FBX operator
40+
bpy.ops.export_scene.fbx(
41+
filepath=path,
42+
use_selection=True,
43+
export_yup=True,
44+
)
45+
```
46+
47+
## The required pattern
48+
49+
```python
50+
# glTF: Unity Y-up
51+
bpy.ops.export_scene.gltf(
52+
filepath=path,
53+
use_selection=True,
54+
export_yup=True,
55+
export_apply=True,
56+
)
57+
58+
# FBX: Unreal centimeters
59+
bpy.ops.export_scene.fbx(
60+
filepath=path,
61+
use_selection=True,
62+
axis_forward="-Z",
63+
axis_up="Y",
64+
global_scale=100.0,
65+
use_mesh_modifiers=True,
66+
)
67+
```
68+
69+
## Why it matters
70+
71+
Unity vs Godot vs Unreal is not one export with different comments. glTF
72+
Y-up bakes `(x, y, z) -> (x, z, -y)` into POSITION. Z-up glTF writes the
73+
Blender coords. FBX uses a different axis pair and can scale to
74+
centimeters without mutating the mesh. Getting the RNA names wrong ships
75+
the default axis and looks like "the engine importer is broken".
76+
77+
## Related
78+
79+
- Skill `engine-export-presets`
80+
- Example `export-preset-axis`
81+
- Example `gltf-export-roundtrip`
82+
- Snippet `export_preset_unity.py`
83+
- Snippet `export_preset_unreal.py`

‎tests/check_import_export_rules.py‎

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
"""Static checks for validate-imported-mesh-scale and
2-
no-unapplied-modifiers-on-export.
1+
"""Static checks for validate-imported-mesh-scale,
2+
no-unapplied-modifiers-on-export, and use-correct-axis-rna-per-exporter.
33
44
Scans snippets/ and templates/**/*.py. examples/ is excluded because several
55
examples are intentional pathology witnesses (unapplied-scale-gltf).
@@ -14,6 +14,7 @@
1414
REQUIRED_RULES = (
1515
"rules/validate-imported-mesh-scale.mdc",
1616
"rules/no-unapplied-modifiers-on-export.mdc",
17+
"rules/use-correct-axis-rna-per-exporter.mdc",
1718
)
1819

1920
IMPORT_RE = re.compile(r"bpy\.ops\.import_scene\.(gltf|fbx)\s*\(")
@@ -27,6 +28,11 @@
2728
)
2829
MODIFIER_NEW_RE = re.compile(r"modifiers\.new")
2930
MODIFIER_APPLY_RE = re.compile(r"modifier_apply")
31+
GLTF_CALL_RE = re.compile(r"bpy\.ops\.export_scene\.gltf\s*\(")
32+
FBX_CALL_RE = re.compile(r"bpy\.ops\.export_scene\.fbx\s*\(")
33+
AXIS_FORWARD_RE = re.compile(r"\baxis_forward\b")
34+
AXIS_UP_RE = re.compile(r"\baxis_up\b")
35+
EXPORT_YUP_RE = re.compile(r"\bexport_yup\b")
3036

3137

3238
def scan_paths(extra):
@@ -40,6 +46,31 @@ def scan_paths(extra):
4046
return paths
4147

4248

49+
def _call_bodies(text, opener_re):
50+
"""Extract argument text of each matching call, paren-matched.
51+
52+
Whole-file scans false-positive a file that correctly calls both
53+
exporters (Unreal glTF plus Unreal FBX). Per-call bodies keep those
54+
legal.
55+
"""
56+
bodies = []
57+
for match in opener_re.finditer(text):
58+
i = match.end()
59+
depth = 1
60+
start = i
61+
while i < len(text) and depth:
62+
char = text[i]
63+
if char == "(":
64+
depth += 1
65+
elif char == ")":
66+
depth -= 1
67+
i += 1
68+
if depth != 0:
69+
continue
70+
bodies.append(text[start : i - 1])
71+
return bodies
72+
73+
4374
def check_text(rel, text):
4475
errors = []
4576
if IMPORT_RE.search(text) and MESH_WORK_RE.search(text):
@@ -55,6 +86,18 @@ def check_text(rel, text):
5586
f"{rel}: export with modifiers.new but no export_apply=True, "
5687
"evaluation_mode, or modifier_apply"
5788
)
89+
for body in _call_bodies(text, GLTF_CALL_RE):
90+
if AXIS_FORWARD_RE.search(body) or AXIS_UP_RE.search(body):
91+
errors.append(
92+
f"{rel}: export_scene.gltf call passes axis_forward or "
93+
"axis_up (FBX RNA; glTF uses export_yup)"
94+
)
95+
for body in _call_bodies(text, FBX_CALL_RE):
96+
if EXPORT_YUP_RE.search(body):
97+
errors.append(
98+
f"{rel}: export_scene.fbx call passes export_yup "
99+
"(glTF RNA; FBX uses axis_forward / axis_up)"
100+
)
58101
return errors
59102

60103

0 commit comments

Comments
 (0)