"""The Grouper let down from BOARD (5.3 m) to POOL (9.3 m) through the deck well on the launch arm, read-only on models/M_Ship_v2_interior_season2.blend (never saved); the Tiger's tg_descent_check.py, adopted from the Critique's run of 2026-10-22 (the docking arm's build/scene_update_rig.check_descent does the same for the Kazah). The Grouper (checks/grouper_tris.npz, antenna folded) in the place of _Tender1 at 9 steps, against the ship's meshes round the well (the carousel's own parts and the Tender instances left out) and against arm 1's own parts. Also reads back the places and the handle the other checks assume. blender -b ../M_Ship_v2_interior_season2.blend --python build/gr_descent_check.py -> build/checks/descent.json""" import bpy, os, math, json import numpy as np from mathutils import Vector from mathutils.bvhtree import BVHTree HERE = os.path.dirname(os.path.abspath(__file__)) VARIANT = os.environ.get("GR_VARIANT", "") d = np.load(os.path.join(HERE, "checks", "grouper_tris%s.npz" % ("_raised" if VARIANT == "raised" else ""))) RV, RT, RN = d["v"], d["t"], d["names"] D = bpy.data rig = next(o for o in D.objects if o.name.startswith("DockArm_Rig") and o.type == 'ARMATURE') h = rig.pose.bones["CTRL_Arm1_Hoist"] info = {"hoist_y": round(h.location.y, 3), "reach_in": h.get("reach_in"), "tenders": {}} for t in sorted([o for o in D.objects if o.name.startswith("_Tender")], key=lambda o: o.name): e = t.matrix_world.to_euler() info["tenders"][t.name] = {"origin": [round(c, 3) for c in t.matrix_world.translation], "rot_deg": [round(math.degrees(a), 2) for a in e]} BOARD, POOL = 5.3, 9.3 lo, hi = Vector((-10.5, -10.5, -11.5)), Vector((16.5, 2.5, 5.5)) skip = ("SK_DockArm_", "_Tender", "Carousel pylon", "DockArm_", "WGT_", "KIT |") dg = bpy.context.evaluated_depsgraph_get() def geo(o): e = o.evaluated_get(dg) me = e.to_mesh() me.calc_loop_triangles() M = e.matrix_world vs = [M @ v.co for v in me.vertices] ts = [tuple(t.vertices) for t in me.loop_triangles] e.to_mesh_clear() return vs, ts ship = {} for o in bpy.context.view_layer.objects: if o.type != 'MESH' or o.library is not None or o.name.startswith(skip) or o.hide_render or o.hide_viewport or not o.visible_get(): continue if any(c.name.startswith(("90", "99", "GAME")) for c in o.users_collection) or any(k in o.name for k in ("_GAME", "_DOCK", "_LOD")): continue bb = [o.matrix_world @ Vector(c) for c in o.bound_box] if (max(v.x for v in bb) < lo.x or min(v.x for v in bb) > hi.x or max(v.y for v in bb) < lo.y or min(v.y for v in bb) > hi.y or max(v.z for v in bb) < lo.z or min(v.z for v in bb) > hi.z): continue vs, ts = geo(o) if ts: ship[o.name] = (BVHTree.FromPolygons(vs, ts), np.array([tuple(v) for v in vs])) mine = [o for o in D.objects if o.name.startswith("SK_DockArm_Arm1_")] tender = D.objects["_Tender1"] y0 = h.location.y rows = [] for i in range(9): y = BOARD + (POOL - BOARD) * i / 8 h.location.y = y rig.update_tag() for o in mine: o.update_tag() tender.update_tag() bpy.context.view_layer.update() dg = bpy.context.evaluated_depsgraph_get() M = np.array(tender.matrix_world) w = RV @ M[:3, :3].T + M[:3, 3] ttree = BVHTree.FromPolygons([Vector(p) for p in w], [tuple(int(k) for k in t) for t in RT]) lo_t, hi_t = w.min(0) - 1.0, w.max(0) + 1.0 cross, gap, near = [], 99.0, None for sn, (st, sv) in ship.items(): n = len(ttree.overlap(st)) if n: cross.append((sn, n)) sel = np.all((sv > lo_t) & (sv < hi_t), axis=1) for v in sv[sel][::2]: dd = ttree.find_nearest(Vector(v))[3] if dd < gap: gap, near = dd, sn arm_hits, arm_gap = [], 99.0 for o in mine: vs, ts = geo(o) if not ts: continue if ttree.overlap(BVHTree.FromPolygons(vs, ts)): arm_hits.append(o.name) arm_gap = min(arm_gap, min(ttree.find_nearest(p)[3] for p in vs)) rows.append(dict(lowered=round(y, 2), tender_origin=[round(c, 3) for c in tender.matrix_world.translation], grouper_z_range=[round(float(w[:, 2].min()), 3), round(float(w[:, 2].max()), 3)], ship_crossings=cross, ship_least_gap=round(gap, 3), nearest_ship=near, arm_hits=arm_hits, arm_least_gap=round(arm_gap, 4))) h.location.y = y0 rig.update_tag() bpy.context.view_layer.update() summary = {"steps": len(rows), "ship_meshes": len(ship), "with_ship_crossings": sum(1 for r in rows if r["ship_crossings"]), "least_ship_gap_m": min(r["ship_least_gap"] for r in rows), "nearest_ship": min(rows, key=lambda r: r["ship_least_gap"])["nearest_ship"], "with_arm_hits": sum(1 for r in rows if r["arm_hits"]), "least_arm_gap_m": min(r["arm_least_gap"] for r in rows)} json.dump({"summary": summary, "ship_state": info, "steps": rows}, open(os.path.join(HERE, "checks", "descent%s.json" % ("_raised" if VARIANT == "raised" else "")), "w"), indent=1) print("DESCENT", json.dumps(summary), json.dumps(info))