# -*- coding: utf-8 -*- """Applies the build's last seating rule to an OrePatches.blend that is already built, so a full rebuild is not needed for it: a cluster whose foot is not under the rock all round is seated that much deeper (plus 3 mm). Updates the .blend, OrePatches.json, build_report.json and the assembled FBX of each patch it touches. blender -b ../OrePatches.blend --python reseat.py """ import bpy, bmesh, os, json, math from mathutils import Vector from mathutils.bvhtree import BVHTree HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) manifest = json.load(open(os.path.join(ROOT, 'OrePatches.json'))) report = json.load(open(os.path.join(HERE, 'build_report.json'))) touched = [] for root in [o for o in bpy.data.objects if o.type == 'EMPTY' and o.name.startswith('OrePatch_')]: rock = next(c for c in root.children if c.name.startswith('SM_OreRock_')) bm = bmesh.new() bm.from_mesh(rock.data) bm.transform(rock.matrix_world) bvh = BVHTree.FromBMesh(bm) bm.free() entry = next(p for p in manifest['patches'] if p['name'] == root.name) feet = [] for ob in sorted([c for c in root.children if '_Ore_' in c.name], key=lambda o: o.name): mw = ob.matrix_world z = (mw.to_3x3() @ Vector((0, 0, 1))).normalized() s = mw.to_scale()[0] foot = 9.0 for k in range(12): p = mw @ Vector((0.175 * math.cos(k * math.pi / 6), 0.175 * math.sin(k * math.pi / 6), 0.0)) hit = bvh.ray_cast(p + z * (0.5 + 0.25 * s), -z) foot = min(foot, (hit[0] - p).dot(z) if hit[0] is not None else -9.0) if -1.0 < foot < 0.0: extra = 0.003 - foot M = ob.matrix_local.copy() M.translation = M.translation - z * extra ob.matrix_local = M row = next(r for r in entry['clusters'] if r['object'] == ob.name) row['pos_m'] = [round(v, 4) for v in M.translation] print('[reseat] %s was %.4f m short, seated %.4f m deeper' % (ob.name, -foot, extra)) foot += extra if root not in touched: touched.append(root) feet.append(foot) report['patches'][root.name]['foot_buried_min_m'] = round(min(feet), 3) report['patches'][root.name]['clusters_with_foot_exposed'] = sum(f < -0.002 for f in feet) bpy.context.view_layer.update() for root in touched: keep = root.location.copy() root.location = (0, 0, 0) bpy.context.view_layer.update() for o in bpy.context.view_layer.objects: o.select_set(False) objs = [root] + list(root.children) hidden = [o for o in objs if o.hide_get() or o.hide_viewport] for c in bpy.data.collections: c.hide_viewport = False for o in objs: o.hide_set(False) o.select_set(True) bpy.context.view_layer.objects.active = root bpy.ops.export_scene.fbx(filepath=os.path.join(ROOT, 'fbx', 'assembled', root.name + '.fbx'), use_selection=True, object_types={'MESH', 'EMPTY'}, use_mesh_modifiers=True, mesh_smooth_type='FACE', add_leaf_bones=False, bake_anim=False, axis_forward='-Y', axis_up='Z', global_scale=1.0, apply_unit_scale=True, apply_scale_options='FBX_SCALE_NONE', use_space_transform=True, bake_space_transform=False, path_mode='STRIP', use_custom_props=True) root.location = keep bpy.data.collections['Collision'].hide_viewport = True json.dump(manifest, open(os.path.join(ROOT, 'OrePatches.json'), 'w'), indent=1) json.dump(report, open(os.path.join(HERE, 'build_report.json'), 'w'), indent=1) if touched: bpy.ops.wm.save_mainfile() print('[reseat] patches touched:', [r.name for r in touched] or 'none')