"""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_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} 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, "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)