"""Check the station: the driver, the canister through its whole slide, topology. Nothing is saved. blender.exe -b ../CanisterStation.blend --python cs_check.py -> build/check_report.json Runs CanisterStation_Root["eject"] from 0 to 1 in 101 steps and tests the canister against the body (triangle overlap in world space, BVH), and measures its smallest clearance to the body (vertices of the canister to the body's surface). """ import bpy, bmesh, json, os, math from mathutils.bvhtree import BVHTree HERE = os.path.dirname(os.path.abspath(__file__)) exec(open(os.path.join(HERE, "cs_lib.py")).read()) def bvh(ob, dg): ev = ob.evaluated_get(dg) me = ev.to_mesh() bm = bmesh.new() bm.from_mesh(me) bm.transform(ev.matrix_world) t = BVHTree.FromBMesh(bm) verts = [v.co.copy() for v in bm.verts] bm.free() ev.to_mesh_clear() return t, verts def main(): sc = bpy.context.scene root = bpy.data.objects["CanisterStation_Root"] body = bpy.data.objects["SM_CanisterStation_Body"] can = bpy.data.objects["SM_CanisterStation_Canister"] rep = {"drivers": {}, "travel": {}, "clashes": [], "audit": {}} for fc in can.animation_data.drivers: d = fc.driver rep["drivers"]["location[%d]" % fc.array_index] = dict(valid=d.is_valid, simple=d.is_simple_expression, expression=d.expression) if root.animation_data: root.animation_data.action = None dg = bpy.context.evaluated_depsgraph_get() tb, _ = bvh(body, dg) gap, where = 1e9, None for k in range(101): root["eject"] = k / 100.0 root.update_tag() # a property set from Python is not tagged by itself sc.frame_set(1 + (k % 2)) dg = bpy.context.evaluated_depsgraph_get() dg.update() tc, vc = bvh(can, dg) pairs = tc.overlap(tb) n = len(pairs) if n: c = dict(eject=k / 100.0, pairs=n) if not rep["clashes"]: # where, the first time: the body's faces and their materials me = body.data names = [m.name for m in me.materials] hit = sorted({j for _, j in pairs})[:12] c["body_faces"] = [(names[me.polygons[j].material_index], [round(x, 3) for x in body.matrix_world @ me.polygons[j].center]) for j in hit] rep["clashes"].append(c) for v in vc[::2]: hit = tb.find_nearest(v) if hit[0] is not None and hit[3] < gap: gap, where = hit[3], (k / 100.0, [round(c, 3) for c in v]) if k in (0, 50, 100): rep["travel"]["eject %.2f" % (k / 100)] = [round(c, 5) for c in can.evaluated_get(dg).matrix_world.translation] rep["smallest_gap_mm"] = round(gap * 1000, 2) rep["smallest_gap_at"] = where for ob in (body, can): rep["audit"][ob.name] = audit(ob) with open(os.path.join(HERE, "check_report.json"), "w") as fh: json.dump(rep, fh, indent=1) print("CHECK OK" if not rep["clashes"] else "CHECK CLASH", json.dumps(rep)) main()