"""The carousel sweep with triangles (adopted from the Critique's run of 2026-10-22), read-only on models/M_Ship_v2_interior_season2.blend (never saved): the Tiger (checks/tiger_tris.npz, tool arm folded) at the three stowed places (radius 6.0, origin 1.0 m under the hub, nose along the tangent as _Tender2/_Tender3 stand), the carousel turned 0..360 in 3 deg steps, against every visible ship mesh round the bay (the carousel's own parts and the Tender instances left out). tg_sweep_check.py measures sideways margins from a table; this is the true 3D clearance. blender -b ../M_Ship_v2_interior_season2.blend --python build/tg_carousel_bvh.py -> build/checks/carousel_bvh.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__)) d = np.load(os.path.join(HERE, "checks", "tiger_tris.npz")) RV, RT = d["v"], d["t"] D = bpy.data hub = np.array(next(o for o in D.objects if o.name.startswith("DockArm_Root")).matrix_world.translation) lo, hi = Vector((hub[0] - 11, hub[1] - 11, hub[2] - 4.5)), Vector((hub[0] + 11, hub[1] + 11, hub[2] + 4.5)) skip = ("SK_DockArm_", "_Tender", "Carousel pylon", "DockArm_", "WGT_", "KIT |") dg = bpy.context.evaluated_depsgraph_get() 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] mn = Vector((min(v.x for v in bb), min(v.y for v in bb), min(v.z for v in bb))) mx = Vector((max(v.x for v in bb), max(v.y for v in bb), max(v.z for v in bb))) if mx.x < lo.x or mn.x > hi.x or mx.y < lo.y or mn.y > hi.y or mx.z < lo.z or mn.z > hi.z: continue 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() if ts: ship.append((o.name, BVHTree.FromPolygons(vs, ts), np.array(mn), np.array(mx))) sub = RV[::3] hits, least, where = [], 9.0, None for ang in range(0, 360, 3): for k in range(3): th = math.radians(-90.0 - 120.0 * k + ang) rz = math.radians(90.0 - 120.0 * k + ang) c, s = math.cos(rz), math.sin(rz) R = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]]) t = hub + np.array([6.0 * math.cos(th), 6.0 * math.sin(th), -1.0]) w = RV @ R.T + t wmn, wmx = w.min(0) - 0.6, w.max(0) + 0.6 cand = [x for x in ship if not (np.any(x[3] < wmn) or np.any(x[2] > wmx))] if not cand: continue tt = BVHTree.FromPolygons([Vector(p) for p in w], [tuple(int(i) for i in f) for f in RT]) ws = sub @ R.T + t for nm, st, _, _ in cand: ov = tt.overlap(st) if ov: hits.append((ang, k + 1, nm, len(ov))) for p in ws: r = st.find_nearest(Vector(p), 0.6) if r[0] is not None and r[3] < least: least, where = r[3], (ang, k + 1, nm, [round(float(v), 2) for v in p]) out = {"ship_meshes": len(ship), "placements": 120 * 3, "with_crossings": len(hits), "crossings": hits[:20], "least_3d_clearance_m": round(least, 3), "where": where} json.dump(out, open(os.path.join(HERE, "checks", "carousel_bvh.json"), "w"), indent=1) print("CAROUSEL", json.dumps(out))