"""Re-imports every exported FBX into an empty scene and compares it with the .blend. The FBX data is turned -90 degrees about Z on export (see build_dockarm.py), so (x, y) is expected back as (y, -x). blender.exe -b --python-exit-code 1 --python verify_fbx.py """ import bpy, os, math, json from mathutils import Vector, Matrix HERE = os.path.dirname(os.path.abspath(__file__)) OUT = os.path.dirname(HERE) FBX = os.path.join(OUT, "fbx") PRE = Matrix.Rotation(-math.pi / 2, 4, 'Z') def bounds(vs): return [round(min(v[i] for v in vs), 4) for i in range(3)] + [round(max(v[i] for v in vs), 4) for i in range(3)] bpy.ops.wm.open_mainfile(filepath=os.path.join(OUT, "DockArm_Carousel.blend")) PARTS = ["SM_DockArm_Mount", "SM_DockArm_Turret", "SM_DockArm_Boom", "SM_DockArm_UpperLink", "SM_DockArm_LowerLink", "SM_DockArm_Head", "SM_DockArm_Claw"] want = {} for n in PARTS: o = bpy.data.objects[n] me = o.data me.calc_loop_triangles() want[n] = dict(b=bounds([PRE @ v.co for v in me.vertices]), tris=len(me.loop_triangles), mats=sorted(m.name for m in me.materials), ucx=sorted(c.name for c in bpy.data.objects if c.parent == o and c.name.startswith("UCX_"))) bpy.context.view_layer.update() asm = {o.name: [round(c, 4) for c in (PRE @ o.matrix_world.translation)] for o in bpy.data.collections["DockArm_Assembly"].objects if o.type in ('MESH', 'EMPTY') and "_Mech" not in o.name} # the hose rigs (armatures) and the mechanism's helper empties are not in the quick-look file _hm = bpy.data.meshes["SM_DockArm_OreHose"] hose_want = dict(b=bounds([PRE @ v.co for v in _hm.vertices]), verts=len(_hm.vertices), bones=sorted(b.name for b in bpy.data.armatures["DockArm_HoseRig"].bones)) n_mesh = sum(1 for o in bpy.data.collections["DockArm_Assembly"].objects if o.type == 'MESH') ok = True rows = [] for n in PARTS: bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=os.path.join(FBX, n + ".fbx")) bpy.context.view_layer.update() o = bpy.data.objects[n] me = o.data me.calc_loop_triangles() got = dict(b=bounds([o.matrix_world @ v.co for v in me.vertices]), tris=len(me.loop_triangles), mats=sorted(m.name.split(".")[0] for m in me.materials), ucx=sorted(c.name for c in bpy.data.objects if c.name.startswith("UCX_"))) same = all(abs(a - b) < 2e-4 for a, b in zip(got["b"], want[n]["b"])) and got["tris"] == want[n]["tris"] and got["mats"] == want[n]["mats"] and got["ucx"] == want[n]["ucx"] ok &= same rows.append((n, "OK" if same else "DIFFERENT", got["tris"], len(got["mats"]), len(got["ucx"]), got["b"] if same else (got, want[n]))) bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=os.path.join(FBX, "SK_DockArm_OreHose.fbx")) bpy.context.view_layer.update() _a = [o for o in bpy.data.objects if o.type == 'ARMATURE'] _m = [o for o in bpy.data.objects if o.type == 'MESH'] hose_got = dict(b=bounds([_m[0].matrix_world @ v.co for v in _m[0].data.vertices]), verts=len(_m[0].data.vertices), bones=sorted(b.name for b in _a[0].data.bones)) hose_ok = (len(_a) == 1 and len(_m) == 1 and hose_got["bones"] == hose_want["bones"] and hose_got["verts"] == hose_want["verts"] and all(abs(a - b) < 2e-3 for a, b in zip(hose_got["b"], hose_want["b"])) and sorted(g.name for g in _m[0].vertex_groups) == hose_want["bones"]) ok &= hose_ok rows.append(("SK_DockArm_OreHose", "OK" if hose_ok else "DIFFERENT", len(hose_got["bones"]), "bones", hose_got["verts"], "verts", hose_got["b"] if hose_ok else (hose_got, hose_want))) bpy.ops.wm.read_factory_settings(use_empty=True) bpy.ops.import_scene.fbx(filepath=os.path.join(FBX, "DockArm_Carousel_Assembled.fbx")) bpy.context.view_layer.update() bad = [] for name, p in asm.items(): o = bpy.data.objects.get(name) if o is None or (Vector(p) - o.matrix_world.translation).length > 2e-4: bad.append((name, p, None if o is None else [round(c, 4) for c in o.matrix_world.translation])) got_mesh = sum(1 for o in bpy.data.objects if o.type == 'MESH') ok &= (not bad) and got_mesh == n_mesh for r in rows: print("FBXCHECK", r) print("FBXCHECK assembled: objects", len(asm), "meshes", got_mesh, "of", n_mesh, "misplaced", bad) print("FBXCHECK", "ALL OK" if ok else "FAILED") if not ok: raise SystemExit(1)