"""Weld rigid stone blocks back into one closed skin.

The former RIGID=1 SPLIT=1 cut gave every block its own vertices along the
joints: once the blocks rotated, cracks opened and the hollow shell showed.
Welding restores the closed surface. Each block still follows a single bone
inside; weights blend only over a few edge rings around its borders, so the
stone bends at the joints like clay instead of tearing.

blender -b -P weld_rigid_blocks.py -- input.glb output.glb [rings]
Also callable from the live Blender MCP: weld_rigid_blocks(mesh_object, rings).
"""
import sys

import bmesh
import bpy


def weld_rigid_blocks(obj, rings=2, dist=0.001):
    """dist is in mesh-local units (centimetres under the 0.01 Meshy armature)."""
    bm = bmesh.new()
    bm.from_mesh(obj.data)
    deform = bm.verts.layers.deform.verify()
    open_before = sum(1 for e in bm.edges if e.is_boundary)

    # Coincident vertices: block cuts and glTF UV seams. Average their weights
    # before merging, otherwise the survivor keeps one side's bone only.
    targets = bmesh.ops.find_doubles(bm, verts=list(bm.verts), dist=dist)["targetmap"]
    groups = {}
    for vert, target in targets.items():
        groups.setdefault(target, [target]).append(vert)
    for target, members in groups.items():
        total = {}
        for member in members:
            for bone, weight in member[deform].items():
                total[bone] = total.get(bone, 0.0) + weight / len(members)
        target[deform].clear()
        for bone, weight in total.items():
            target[deform][bone] = weight
    bmesh.ops.weld_verts(bm, targetmap=targets)

    # Spread the joint blend a few rings inward. A block interior whose
    # neighbours all share its single bone is left exactly as it was.
    bm.verts.ensure_lookup_table()
    for _ in range(rings):
        current = [dict(v[deform].items()) for v in bm.verts]
        for v in bm.verts:
            neighbours = [current[e.other_vert(v).index] for e in v.link_edges]
            own = current[v.index]
            if not neighbours or (len(own) == 1 and all(n.keys() == own.keys() for n in neighbours)):
                continue
            mixed = {bone: weight * 0.5 for bone, weight in own.items()}
            for n in neighbours:
                for bone, weight in n.items():
                    mixed[bone] = mixed.get(bone, 0.0) + weight * 0.5 / len(neighbours)
            # glTF keeps four influences: drop the weakest, then normalise.
            kept = sorted(mixed.items(), key=lambda item: item[1], reverse=True)[:4]
            scale = sum(weight for _, weight in kept)
            v[deform].clear()
            for bone, weight in kept:
                v[deform][bone] = weight / scale

    open_after = sum(1 for e in bm.edges if e.is_boundary)
    blended = sum(1 for v in bm.verts if len(v[deform]) > 1)
    bm.to_mesh(obj.data)
    bm.free()
    obj.data.update()
    return {"welded": len(targets), "open_edges_before": open_before,
            "open_edges_after": open_after, "blended_vertices": blended,
            "vertices": len(obj.data.vertices)}


def main():
    args = sys.argv[sys.argv.index("--") + 1:]
    source, destination = args[:2]
    rings = int(args[2]) if len(args) > 2 else 2
    bpy.ops.wm.read_factory_settings(use_empty=True)
    bpy.ops.import_scene.gltf(filepath=source)
    for o in list(bpy.context.scene.objects):
        if o.type == "MESH" and o.name.startswith("Icosphere"):
            bpy.data.objects.remove(o)
    mesh = next(o for o in bpy.context.scene.objects if o.type == "MESH")
    print("WELDED", weld_rigid_blocks(mesh, rings), flush=True)
    bpy.ops.export_scene.gltf(filepath=destination, export_format="GLB",
                              export_animations=True, export_animation_mode="ACTIONS")


if __name__ == "__main__":
    main()
