"""Grouper: twisted and folded quads in SM_Grouper_Hull (read-only): every quad whose two triangles (either diagonal) differ by more than 25 deg, by material and place; over 90 deg = a fold. -> printed (TWIST ...) blender.exe -b Tender_Grouper.blend --python build/gr_fold_audit.py""" import bpy, bmesh, math, json, os, collections from mathutils import Vector o = bpy.data.objects["SM_Grouper_Hull"] bm = bmesh.new(); bm.from_mesh(o.data) tw = [] for f in bm.faces: if len(f.verts) != 4: continue a, b, c, d = [v.co for v in f.verts] n1 = (b - a).cross(c - a); n2 = (c - a).cross(d - a) n3 = (b - a).cross(d - a); n4 = (c - b).cross(d - b) if n1.length < 1e-12 or n2.length < 1e-12: continue ang = math.degrees(n1.angle(n2)) ang2 = math.degrees(n3.angle(n4)) if n3.length > 1e-12 and n4.length > 1e-12 else 0 if max(ang, ang2) > 25: tw.append((max(ang, ang2), tuple(round(v, 3) for v in f.calc_center_median()), o.data.materials[f.material_index].name)) tw.sort(reverse=True) print("TWIST faces >25deg:", len(tw)) for t in tw[:25]: print("TWIST", t) import collections c = collections.Counter() for ang, p, m in tw: c[(m, round(p[1] * 2) / 2)] += 1 print("TWIST by material/y:", sorted(c.items(), key=lambda kv: -kv[1])[:30]) big = [t for t in tw if t[0] > 90] c2 = collections.Counter((m, round(p[1] * 4) / 4, round(p[2] * 4) / 4) for a, p, m in big) print("TWIST >90:", len(big), sorted(c2.items(), key=lambda kv: -kv[1])[:20]) for t in big[:30]: print("BIG", round(t[0]), t[1], t[2]) # round 1: counts over 20 / 25 / 90 deg, for the whole hull mesh and for its skin grid (the largest loose piece) alone part = {} pid = 0 bm.faces.ensure_lookup_table() for f in bm.faces: if f.index in part: continue st = [f] part[f.index] = pid while st: g = st.pop() for e in g.edges: for h in e.link_faces: if h.index not in part: part[h.index] = pid st.append(h) pid += 1 size = collections.Counter(part.values()) skin = max(size, key=lambda k: size[k]) allq = [] for f in bm.faces: if len(f.verts) != 4: continue a, b, c, d = [v.co for v in f.verts] n1 = (b - a).cross(c - a); n2 = (c - a).cross(d - a); n3 = (b - a).cross(d - a); n4 = (c - b).cross(d - b) if min(n.length for n in (n1, n2, n3, n4)) < 1e-14: continue allq.append((max(math.degrees(n1.angle(n2)), math.degrees(n3.angle(n4))), part[f.index] == skin)) res = {"quads": len(allq), "over_20": sum(1 for t, s_ in allq if t > 20), "over_25": sum(1 for t, s_ in allq if t > 25), "over_90": sum(1 for t, s_ in allq if t > 90), "skin_quads": sum(1 for t, s_ in allq if s_), "skin_over_20": sum(1 for t, s_ in allq if s_ and t > 20), "skin_over_25": sum(1 for t, s_ in allq if s_ and t > 25), "skin_over_90": sum(1 for t, s_ in allq if s_ and t > 90), "pieces": len(size)} json.dump(res, open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "checks", "folds.json"), "w"), indent=1) print("TWIST summary", json.dumps(res))