"""Spearfish: the moving parts through their travel against the static exterior (exec'd in Blender, or headless with blender -b Tender_Spearfish.blend --python build/motion_check.py). Writes checks/motion.json; leaves the rig as found. * top hatch: 0 .. 100 deg in 2 deg steps, intersections with the hull and least gap; * whip masts: 0 .. 1 folded in 0.05 steps, intersections with the hull and least gap.""" import bpy, os, json from mathutils import Vector from mathutils.bvhtree import BVHTree HERE = r"D:\HADAL - The Descent\models\tender-spearfish\build" root = bpy.data.objects["Spearfish_Root"] for ob in bpy.data.objects: if ob.animation_data: for fc in ob.animation_data.drivers: fc.driver.expression = fc.driver.expression fc.driver.is_valid = True def world_tree(o): dg = bpy.context.evaluated_depsgraph_get() ev = o.evaluated_get(dg) me = ev.to_mesh() M = ev.matrix_world vs = [M @ v.co for v in me.vertices] ps = [tuple(p.vertices) for p in me.polygons] ev.to_mesh_clear() return BVHTree.FromPolygons(vs, ps), vs hull_t, hull_v = world_tree(bpy.data.objects["SM_Spearfish_Hull"]) def probe(o, pivot=None, skip_r=0.0): """intersections and least gap; faces within skip_r of the pivot (a mast in its socket, a lid on its hinge) left out""" dg = bpy.context.evaluated_depsgraph_get() ev = o.evaluated_get(dg) me = ev.to_mesh() M = ev.matrix_world vs = [M @ v.co for v in me.vertices] pv = (M @ Vector(pivot)) if pivot is not None else None ps = [tuple(p.vertices) for p in me.polygons if pv is None or min((vs[i] - pv).length for i in p.vertices) > skip_r] ev.to_mesh_clear() used = sorted({i for p in ps for i in p}) t = BVHTree.FromPolygons(vs, ps) vs = [vs[i] for i in used] hits = len(t.overlap(hull_t)) gap = min(hull_t.find_nearest(p)[3] for p in vs[::3]) return hits, round(gap, 4) keep = (root["masts_folded"], root["hatch_open_deg"]) out = {"hatch": [], "masts": []} for deg in range(0, 101, 2): root["hatch_open_deg"] = float(deg); root.update_tag(); bpy.context.view_layer.update() h, g = probe(bpy.data.objects["SM_Spearfish_Hatch"]) # the lid sits 14 mm proud; its hinge is its aft edge out["hatch"].append([deg, h, g]) root["hatch_open_deg"] = 0.0 for j in range(21): root["masts_folded"] = j / 20; root.update_tag(); bpy.context.view_layer.update() row = [j / 20] for nm in ("SM_Spearfish_Mast_L", "SM_Spearfish_Mast_R"): row += list(probe(bpy.data.objects[nm], pivot=(0, 0, 0), skip_r=0.075)) # the whip's foot turns in its socket out["masts"].append(row) root["masts_folded"], root["hatch_open_deg"] = keep root.update_tag(); bpy.context.view_layer.update() summary = {"hatch_hits": sum(r[1] for r in out["hatch"]), "hatch_least_gap_m": min(r[2] for r in out["hatch"][1:]), "hatch_hits_shut": out["hatch"][0][1], "mast_hits": sum(r[1] + r[3] for r in out["masts"]), "mast_least_gap_m": min(min(r[2], r[4]) for r in out["masts"])} out["summary"] = summary json.dump(out, open(os.path.join(HERE, "checks", "motion.json"), "w"), indent=1) print("MOTION", json.dumps(summary)) result = summary