"""Critique 2: checks the rig check does not make. Read-only (the file is never saved). 1. The end against its door, measured as the relative transform door^-1 @ part for the tool, the four distal fingers (A) and the plate (B), frames 1-120: its largest change from frame 1 (mm, deg). 2. The carriage stays on its rail line (world y, z constant) and the carriage's x against the rig's c. 3. The version switch done the way the user does it (set the property, no frame change) at frames 48 and 90: is the version that appears posed for the current frame, or stale? 4. Door fixtures and energy-chain links against the ship context (deck, rails, end walls, lining, basin, rim, lamps), the doors against the rail kits and chains, frames 1-120 (crossing = triangles that meet and each reach > 0.2 mm through the other's plane, as the rig check defines it). 5. Grip: the four hooks' nearest distance to the knob fixture (A), the plate to its pad (B), every 6th frame. 6. Solver empties: shown in the viewport? Material viewport colours. Mesh-only bounds of the showroom arms. blender -b --factory-startup "" --python previews/critique2/crit2_rigextra.py -- """ import json import math import sys import bpy import numpy as np from mathutils import Matrix, Vector from mathutils.bvhtree import BVHTree OUT = sys.argv[sys.argv.index("--") + 1] if "--" in sys.argv else "crit2_rigextra.json" EPS = 0.0002 R = {} sf = bpy.data.scenes["DoorArm | Ship fit"] win = bpy.context.window_manager.windows if win: win[0].scene = sf ctl = bpy.data.objects["DoorArm | CONTROLS"] DOORS = {"AFT": bpy.data.objects["Moonpool_Door_Aft"], "FORE": bpy.data.objects["Moonpool_Door_Fore"]} def dg(): return bpy.context.evaluated_depsgraph_get() def mw(o): return o.evaluated_get(dg()).matrix_world.copy() def setv(v): ctl["version"] = v ctl.update_tag() def rel_err(M0, M1): d = (M1.translation - M0.translation).length * 1000 q = (M0.to_quaternion().inverted() @ M1.to_quaternion()) a = math.degrees(q.angle) a = min(a, 360 - a) return d, a # ---------------------------------------------------------------- 1 + 2: the end against the door, the carriage line ends = {} for ver in ("A", "B"): setv(0 if ver == "A" else 1) for key in ("AFT", "FORE"): p = "DoorArm_%s_%s | " % (key, ver) names = ["flange"] + (["tool A hub"] + ["finger %s distal" % w for w in ("top", "left", "bottom", "right")] if ver == "A" else ["tool B"]) obs = {n: bpy.data.objects[p + n] for n in names} car = bpy.data.objects[p + "carriage"] door = DOORS[key] ref = {} worst = {n: [0.0, 0.0, 0] for n in names} cy, cz, cx = [], [], [] for f in range(1, 121): sf.frame_set(f) Md = mw(door) for n, o in obs.items(): rel = Md.inverted() @ mw(o) if f == 1: ref[n] = rel else: e_mm, e_deg = rel_err(ref[n], rel) if e_mm > worst[n][0]: worst[n][0] = e_mm worst[n][2] = f worst[n][1] = max(worst[n][1], e_deg) Mc = mw(car) cx.append(Mc.translation.x) cy.append(Mc.translation.y) cz.append(Mc.translation.z) ends["%s_%s" % (key, ver)] = { "relative_to_door_max_change": {n: [round(w[0], 3), round(w[1], 4), w[2]] for n, w in worst.items()}, "carriage_world_x": [round(min(cx), 4), round(max(cx), 4)], "carriage_world_y_spread_mm": round((max(cy) - min(cy)) * 1000, 4), "carriage_world_z_spread_mm": round((max(cz) - min(cz)) * 1000, 4), "carriage_y_z": [round(cy[0], 4), round(cz[0], 4)], } R["end_vs_door"] = ends # ---------------------------------------------------------------- 3: the switch as the user does it D_T = {"A": 0.63, "B": 0.326} import os # noqa: E402 with open(os.path.join(os.path.dirname(bpy.data.filepath), "design.json")) as fh: DES = json.load(fh) def flange_err(key, ver): dr = DES["doors"][key] door = DOORS[key] fl = mw(bpy.data.objects["DoorArm_%s_%s | flange" % (key, ver)]) P = mw(door).translation + Vector(dr["attach_local"]) WC = P - Vector(dr["tool_axes_world"]["x_t"]) * D_T[ver] return round((fl.translation - WC).length * 1000, 2) sw = [] setv(0) sf.frame_set(1) sf.frame_set(48) # A shown, B hidden (stale since it was hidden) setv(1) # the user types 1, no frame change bpy.context.view_layer.update() sw.append(["frame 48, switched A->B, no frame change", {k: flange_err(k, "B") for k in ("AFT", "FORE")}]) sf.frame_set(90) sw.append(["then frame 90 (B shown)", {k: flange_err(k, "B") for k in ("AFT", "FORE")}]) setv(0) bpy.context.view_layer.update() sw.append(["frame 90, switched B->A, no frame change", {k: flange_err(k, "A") for k in ("AFT", "FORE")}]) sf.frame_set(91) sw.append(["then frame 91 (A shown)", {k: flange_err(k, "A") for k in ("AFT", "FORE")}]) vis = {} for v in (0, 1): setv(v) sf.frame_set(30) vis[v] = {"A shown": sum(1 for o in sf.objects if o.get("version") == "A" and not o.hide_viewport), "B shown": sum(1 for o in sf.objects if o.get("version") == "B" and not o.hide_viewport)} R["switch_like_the_user"] = {"flange_error_mm": sw, "shown_objects": vis} setv(0) sf.frame_set(1) # ---------------------------------------------------------------- 4: fixtures, chains and doors against the context class G: def __init__(self, ob): self.ob = ob me = ob.data n = len(me.vertices) co = np.empty(n * 3) me.vertices.foreach_get("co", co) self.local = co.reshape(n, 3) me.calc_loop_triangles() nt = len(me.loop_triangles) tv = np.empty(nt * 3, dtype=np.int64) me.loop_triangles.foreach_get("vertices", tv) self.tris = tv.reshape(nt, 3) self.tl = [tuple(t) for t in self.tris.tolist()] def upd(self): A = np.array(mw(self.ob)) self.w = self.local @ A[:3, :3].T + A[:3, 3] self.lo, self.hi = self.w.min(0), self.w.max(0) self.t = None def bvh(self): if self.t is None: self.t = BVHTree.FromPolygons(self.w.tolist(), self.tl, all_triangles=True) return self.t def strad(P, Q): 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 def cross(a, b): if np.any(a.lo > b.hi + 0.001) or np.any(b.lo > a.hi + 0.001): return 0 k = 0 for i, j in a.bvh().overlap(b.bvh()): P = [Vector(a.w[v]) for v in a.tris[i]] Q = [Vector(b.w[v]) for v in b.tris[j]] if strad(P, Q) and strad(Q, P): k += 1 return k def near(a, b, cap=0.2): if np.any(a.lo > b.hi + cap) or np.any(b.lo > a.hi + cap): return None tb = b.bvh() best = cap idx = np.arange(len(a.w))[:: max(1, len(a.w) // 3000)] for vi in idx: h = tb.find_nearest(Vector(a.w[vi]), best) if h[0] is not None and h[3] < best: best = h[3] return round(best, 4) ctx_names = [o.name for o in sf.objects if o.type == 'MESH' and o.get("reference_only") and not o.hide_render and not o.name.startswith("Moonpool_Door")] ctx = {n: G(bpy.data.objects[n]) for n in ctx_names} doors = {k: G(o) for k, o in DOORS.items()} kits = {k: G(bpy.data.objects["DoorArm_%s | rail kit" % k]) for k in ("AFT", "FORE")} chains = {k: [G(o) for o in sf.objects if o.name.startswith("DoorArm_%s | chain link" % k)] for k in ("AFT", "FORE")} res = {"context": ctx_names, "fixture_x_context": {}, "chain_x_context": {}, "door_x_kit_chain": {}, "near": {}} sf.frame_set(1) for g in list(ctx.values()) + list(kits.values()): # static: the context (doors apart) and the rail kits g.upd() for ver in ("A", "B"): setv(0 if ver == "A" else 1) fx = {k: G(bpy.data.objects["DoorArm_%s | door fixture %s" % (k, ver)]) for k in ("AFT", "FORE")} for f in range(1, 121): sf.frame_set(f) for g in list(doors.values()) + list(fx.values()): g.upd() if ver == "A": for k in ("AFT", "FORE"): for g in chains[k]: g.upd() for k in ("AFT", "FORE"): others = dict(ctx) others["rail kit " + k] = kits[k] for n, g in others.items(): c = cross(fx[k], g) if c: res["fixture_x_context"].setdefault("%s %s x %s" % (k, ver, n), []).append([f, c]) if f % 12 == 1: d = near(fx[k], g) kk = "fixture %s %s ~ %s" % (k, ver, n) if d is not None and (kk not in res["near"] or d < res["near"][kk]): res["near"][kk] = d if ver == "A": for g in chains[k]: for n, h in ctx.items(): c = cross(g, h) if c: res["chain_x_context"].setdefault("%s %s x %s" % (k, g.ob.name, n), []).append([f, c]) for dk, dgeo in doors.items(): c = cross(g, dgeo) if c: res["door_x_kit_chain"].setdefault("%s x %s" % (dk, g.ob.name), []).append([f, c]) for dk, dgeo in doors.items(): c = cross(dgeo, kits[k]) if c: res["door_x_kit_chain"].setdefault("%s x rail kit %s" % (dk, k), []).append([f, c]) for k in list(res["fixture_x_context"]): res["fixture_x_context"][k] = res["fixture_x_context"][k][:10] R["extra_crossings"] = res setv(0) sf.frame_set(1) # ---------------------------------------------------------------- 5: grip on the knob, plate on the pad grip = {} for ver in ("A", "B"): setv(0 if ver == "A" else 1) for key in ("AFT", "FORE"): fx = G(bpy.data.objects["DoorArm_%s | door fixture %s" % (key, ver)]) parts = ["finger %s distal" % w for w in ("top", "left", "bottom", "right")] if ver == "A" else ["tool B"] gs = [G(bpy.data.objects["DoorArm_%s_%s | %s" % (key, ver, n)]) for n in parts] worst = {} for f in range(1, 121, 6): sf.frame_set(f) fx.upd() for g in gs: g.upd() d = near(g, fx, cap=0.1) n = g.ob.name.split(" | ")[-1] lo, hi = worst.get(n, [9, -9]) if d is not None: worst[n] = [min(lo, d), max(hi, d)] else: worst[n] = [lo, hi, "> 0.1 m at frame %d" % f] grip["%s_%s" % (key, ver)] = worst R["grip_and_plate_m"] = grip setv(0) sf.frame_set(1) # ---------------------------------------------------------------- 6: what shows sol = [o for o in sf.objects if o.users_collection and o.users_collection[0].name == "DoorArm | solver (hidden)"] R["solver_empties"] = {"count": len(sol), "shown_in_viewport": sum(1 for o in sol if not (o.hide_get() or o.hide_viewport)), "display": sorted(set("%s %.2f" % (o.empty_display_type, o.empty_display_size) for o in sol))} R["material_viewport_colours"] = {m.name: [round(c, 3) for c in m.diffuse_color] for m in bpy.data.materials if m.name.startswith("M_DoorArm")} show = bpy.data.scenes["DoorArm | Showroom"] bb = {} for v in ("A", "B"): xs = [] zs = [] for o in show.objects: if o.name.startswith("Showroom | arm %s |" % v) and o.type == 'MESH': M = o.matrix_world for c in o.data.vertices: p = M @ c.co xs.append(p.x) zs.append(p.z) bb[v] = [round(min(xs), 3), round(max(xs), 3), round(min(zs), 3), round(max(zs), 3)] R["showroom_mesh_bounds_x_z"] = bb with open(OUT, "w") as fh: json.dump(R, fh, indent=1) print("[crit2] rigextra written", OUT, flush=True)