|
| 1 | +"""Static checks for validate-imported-mesh-scale and |
| 2 | +no-unapplied-modifiers-on-export. |
| 3 | +
|
| 4 | +Scans snippets/ and templates/**/*.py. examples/ is excluded because several |
| 5 | +examples are intentional pathology witnesses (unapplied-scale-gltf). |
| 6 | +""" |
| 7 | +import glob |
| 8 | +import os |
| 9 | +import re |
| 10 | +import sys |
| 11 | + |
| 12 | +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 13 | + |
| 14 | +REQUIRED_RULES = ( |
| 15 | + "rules/validate-imported-mesh-scale.mdc", |
| 16 | + "rules/no-unapplied-modifiers-on-export.mdc", |
| 17 | +) |
| 18 | + |
| 19 | +IMPORT_RE = re.compile(r"bpy\.ops\.import_scene\.(gltf|fbx)\s*\(") |
| 20 | +EXPORT_RE = re.compile( |
| 21 | + r"bpy\.ops\.(export_scene\.gltf|export_scene\.fbx|wm\.usd_export)\s*\(" |
| 22 | +) |
| 23 | +MESH_WORK_RE = re.compile(r"bmesh\.|modifiers\.new|from_pydata|foreach_set") |
| 24 | +UNIT_SCALE_RE = re.compile(r"unit_settings|scale_length|global_scale") |
| 25 | +EXPORT_EVAL_RE = re.compile( |
| 26 | + r"export_apply\s*=\s*True|evaluation_mode\s*=|use_mesh_modifiers\s*=\s*True" |
| 27 | +) |
| 28 | +MODIFIER_NEW_RE = re.compile(r"modifiers\.new") |
| 29 | +MODIFIER_APPLY_RE = re.compile(r"modifier_apply") |
| 30 | + |
| 31 | + |
| 32 | +def scan_paths(extra): |
| 33 | + paths = [] |
| 34 | + paths.extend(glob.glob(os.path.join(ROOT, "snippets", "*.py"))) |
| 35 | + paths.extend( |
| 36 | + glob.glob(os.path.join(ROOT, "templates", "**", "*.py"), recursive=True) |
| 37 | + ) |
| 38 | + for item in extra: |
| 39 | + paths.append(item if os.path.isabs(item) else os.path.join(ROOT, item)) |
| 40 | + return paths |
| 41 | + |
| 42 | + |
| 43 | +def check_text(rel, text): |
| 44 | + errors = [] |
| 45 | + if IMPORT_RE.search(text) and MESH_WORK_RE.search(text): |
| 46 | + if "transform_apply" not in text or not UNIT_SCALE_RE.search(text): |
| 47 | + errors.append( |
| 48 | + f"{rel}: import_scene gltf/fbx plus mesh work without " |
| 49 | + "transform_apply and a unit-scale check " |
| 50 | + "(unit_settings, scale_length, or global_scale)" |
| 51 | + ) |
| 52 | + if EXPORT_RE.search(text) and MODIFIER_NEW_RE.search(text): |
| 53 | + if not EXPORT_EVAL_RE.search(text) and not MODIFIER_APPLY_RE.search(text): |
| 54 | + errors.append( |
| 55 | + f"{rel}: export with modifiers.new but no export_apply=True, " |
| 56 | + "evaluation_mode, or modifier_apply" |
| 57 | + ) |
| 58 | + return errors |
| 59 | + |
| 60 | + |
| 61 | +def main(argv): |
| 62 | + errors = [] |
| 63 | + for rule in REQUIRED_RULES: |
| 64 | + path = os.path.join(ROOT, rule) |
| 65 | + if not os.path.isfile(path): |
| 66 | + errors.append(f"missing rule file {rule}") |
| 67 | + |
| 68 | + extra = argv[1:] |
| 69 | + for path in scan_paths(extra): |
| 70 | + if not os.path.isfile(path): |
| 71 | + errors.append(f"missing scan path {path}") |
| 72 | + continue |
| 73 | + rel = os.path.relpath(path, ROOT).replace("\\", "/") |
| 74 | + text = open(path, encoding="utf-8").read() |
| 75 | + errors.extend(check_text(rel, text)) |
| 76 | + |
| 77 | + if errors: |
| 78 | + for err in errors: |
| 79 | + print(f"ERROR: {err}", file=sys.stderr) |
| 80 | + return 1 |
| 81 | + print("import/export anti-pattern checks passed.") |
| 82 | + return 0 |
| 83 | + |
| 84 | + |
| 85 | +if __name__ == "__main__": |
| 86 | + sys.exit(main(sys.argv)) |
0 commit comments