"""Grouper: every moving part through its whole travel against the static model (read-only; nothing saved): blender.exe -b Tender_Grouper.blend --python build/gr_motion_check.py -> build/checks/motion.json * the hatch 0 .. 100 deg (2.5 deg steps) against the hull (the collar, its bore and the hinge's fixed knuckles in it); * each side door 0 .. 95 deg (2.5 deg steps) against the hull (the doorway's trim and sleeve, the hinge's fixed knuckles); * the antenna 0 .. 70 deg (2.5 deg steps) against the hull (its clevis); the hinge pin through its knuckle (by design) is left out: the hull's faces within 13 mm of the hinge axis and within 4 cm of the centre line; * the rotor through one blade pitch (72 deg, 3 deg steps) against the hull (the stator, the hubs, the duct's inner wall). Per step: intersecting triangle pairs (BVH overlap) and the least gap (the moving part's vertices to the static faces). The drivers are re-armed and the frame changed so each pose is the driven one (blender-headless gotchas).""" import bpy, os, json, math from mathutils import Vector from mathutils.bvhtree import BVHTree HERE = os.path.dirname(os.path.abspath(__file__)) os.makedirs(os.path.join(HERE, "checks"), exist_ok=True) sc = bpy.context.scene root = bpy.data.objects["Grouper_Root"] hull = bpy.data.objects["SM_Grouper_Hull"] def rearm(): 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 geo(o): dg = bpy.context.evaluated_depsgraph_get() ev = o.evaluated_get(dg) me = ev.to_mesh() me.calc_loop_triangles() Mx = ev.matrix_world.copy() vs = [Mx @ v.co for v in me.vertices] ts = [tuple(t.vertices) for t in me.loop_triangles] ev.to_mesh_clear() return vs, ts def pose(prop, val, movers): root[prop] = val root.update_tag() for o in movers: o.update_tag() sc.frame_set(sc.frame_current + 1) bpy.context.view_layer.update() rearm() HV, HT = geo(hull) def static_tree(exclude=None): keep = HT if exclude is None else [t for t in HT if not exclude((HV[t[0]] + HV[t[1]] + HV[t[2]]) / 3)] return BVHTree.FromPolygons(HV, keep), keep def sweep(obj, prop, values, exclude=None, expect=None): tree, keep = static_tree(exclude) start = root.get(prop, 0.0) rows = [] for v in values: pose(prop, v, [obj]) mv, mt = geo(obj) mtree = BVHTree.FromPolygons(mv, mt) ov = mtree.overlap(tree) g = min(tree.find_nearest(p)[3] for p in mv) where = sorted({tuple(round(c, 2) for c in ((HV[keep[j][0]] + HV[keep[j][1]] + HV[keep[j][2]]) / 3)) for i, j in ov})[:4] rows.append({"value": v, "pairs": len(ov), "least_gap_m": round(g, 4), "hull_faces_hit": [list(w) for w in where], "rot_deg": [round(math.degrees(a), 2) for a in obj.matrix_world.to_euler()]}) pose(prop, start, [obj]) moved = rows[-1]["rot_deg"] != rows[0]["rot_deg"] return {"steps": len(rows), "with_intersections": sum(1 for r in rows if r["pairs"]), "least_gap_m": min(r["least_gap_m"] for r in rows), "driver_moves_it": moved, "first_hit": next((r["value"] for r in rows if r["pairs"]), None), "rows": rows} out = {} hatch = bpy.data.objects["SM_Grouper_Hatch"] out["hatch"] = sweep(hatch, "hatch_open_deg", [2.5 * k for k in range(41)]) for tag in ("L", "R"): d = bpy.data.objects["SM_Grouper_Door_" + tag] out["door_" + tag] = sweep(d, "door_%s_deg" % tag.lower(), [2.5 * k for k in range(39)]) ant = bpy.data.objects["SM_Grouper_Antenna"] ax = ant.matrix_world.translation.copy() out["antenna"] = sweep(ant, "antenna_fold_deg", [2.5 * k for k in range(29)], exclude=lambda c: abs(c.x) < 0.04 and math.hypot(c.y - ax.y, c.z - ax.z) < 0.013) rot = bpy.data.objects["SM_Grouper_Rotor"] out["rotor"] = sweep(rot, "rotor_turns", [k / 120.0 for k in range(25)]) summary = {k: {kk: v[kk] for kk in ("steps", "with_intersections", "least_gap_m", "driver_moves_it", "first_hit")} for k, v in out.items()} json.dump({"summary": summary, "parts": out}, open(os.path.join(HERE, "checks", "motion.json"), "w"), indent=1) print("MOTION", json.dumps(summary))