"""Critique 1: read-only inspection of DoorRobotArm.blend (never saved). Writes crit1_inspect.json next to this file.""" import bpy, bmesh, json, os, math, sys from mathutils import Vector from mathutils.bvhtree import BVHTree OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "crit1_inspect.json") HERE_BUILD = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "build") sys.path.insert(0, HERE_BUILD) rep = {} sc = bpy.data.scenes["DoorArm | Showroom"] sc.frame_set(1) dg = sc.view_layers[0].depsgraph dg.update() # objects of the showroom arms objs = [] for ob in sc.objects: if not ob.name.startswith("Showroom | arm"): continue d = dict(name=ob.name, type=ob.type, parent=ob.parent.name if ob.parent else None, loc=[round(x, 5) for x in ob.location], rot=[round(math.degrees(x), 4) for x in ob.rotation_euler], scale=[round(x, 5) for x in ob.scale], link=ob.get("link"), version=ob.get("version"), mods=[m.type for m in getattr(ob, "modifiers", [])], cons=[c.type for c in ob.constraints], data=ob.data.name if ob.data else None, hide_render=ob.hide_render) if ob.type == 'MESH' and ob.data.get("joint_A") is not None: root = ob while root.parent is not None: root = root.parent ja = Vector(ob.data["joint_A"]) exp = root.matrix_world @ ja d["origin_err_mm"] = round((ob.matrix_world.translation - exp).length * 1000, 3) d["verts"] = len(ob.data.vertices) objs.append(d) rep["objects"] = objs # every modifier in the file rep["modifiers_anywhere"] = sorted(set((o.name, m.type) for o in bpy.data.objects for m in getattr(o, "modifiers", [])))[:200] # non-manifold edges: where def nonmanifold(me): bm = bmesh.new(); bm.from_mesh(me) lay = bm.faces.layers.int.get('part') ids = list(me.get("parts", [])) out = [] for e in bm.edges: if len(e.link_faces) > 2: parts = sorted(set(ids[f[lay]] if lay is not None and f[lay] < len(ids) else "?" for f in e.link_faces)) c = (e.verts[0].co + e.verts[1].co) / 2 out.append(dict(parts=parts, at=[round(x, 4) for x in c], nfaces=len(e.link_faces))) bm.free() return out rep["nonmanifold"] = {me.name: nonmanifold(me) for me in bpy.data.meshes if me.users and me.name.startswith("DoorArm |")} rep["nonmanifold"] = {k: v for k, v in rep["nonmanifold"].items() if v} # curves against every arm object, with where along the curve def tris_world(ob): ev = ob.evaluated_get(dg) me = ev.to_mesh() M = ev.matrix_world verts = [M @ v.co for v in me.vertices] me.calc_loop_triangles() tris = [tuple(lt.vertices) for lt in me.loop_triangles] polys = [lt.polygon_index for lt in me.loop_triangles] lay = me.attributes.get('part') pv = [lay.data[p].value for p in polys] if lay is not None else [-1] * len(polys) ev.to_mesh_clear() return verts, tris, pv def straddle(p, q, eps=0.0002): n = (q[1] - q[0]).cross(q[2] - q[0]) if n.length < 1e-14: return False n.normalize() d = [(x - q[0]).dot(n) for x in p] return max(d) > eps and min(d) < -eps curv = [] for v in ("A", "B"): arm = [o for o in sc.objects if o.name.startswith("Showroom | arm %s |" % v)] meshes = [o for o in arm if o.type == 'MESH'] curves = [o for o in arm if o.type == 'CURVE'] for cu in curves: cv, ct, _ = tris_world(cu) tc = BVHTree.FromPolygons(cv, ct, all_triangles=True) # curve end points in world sp = cu.data.splines[0] ends = [cu.matrix_world @ sp.bezier_points[0].co, cu.matrix_world @ sp.bezier_points[-1].co] for ob in meshes: mv, mt, mp = tris_world(ob) tm = BVHTree.FromPolygons(mv, mt, all_triangles=True) ids = list(ob.data.get("parts", [])) hits = {} for i, j in tc.overlap(tm): p = [cv[k] for k in ct[i]]; q = [mv[k] for k in mt[j]] if straddle(p, q) and straddle(q, p): c = (p[0] + p[1] + p[2]) / 3 dend = min((c - e).length for e in ends) part = ids[mp[j]] if 0 <= mp[j] < len(ids) else "?" h = hits.setdefault(part, [0, 9.0]) h[0] += 1 h[1] = min(h[1], dend) for part, (n, dmin) in hits.items(): curv.append(dict(curve=cu.name, obj=ob.name, part=part, n=n, min_dist_from_end_m=round(dmin, 3))) rep["curve_hits"] = curv # showroom: are both versions really side by side? (positions of roots) rep["roots"] = {o.name: [round(x, 4) for x in o.matrix_world.translation] for o in sc.objects if o.parent is None and o.name.startswith("Showroom | arm")} json.dump(rep, open(OUT, "w"), indent=1) print("[crit1] wrote", OUT, flush=True)