"""Door robot arm: the rig check (stage 'rig_check' -> build/rig_report.json). Reads the file, changes nothing. For both doors and both versions, at every frame of the doors' animation (1-120, step 1): - pose: the flange (the tool's frame) against the door target: the wrist centre must sit at the door's attach point minus d_t along the tool axis, with the tool axes = the door's (the door only translates): position error (mm) and angle error (deg), max over the timeline; - the carriage: its x inside its travel, its end faces' distance to the end-stop buffers, the arm's distance to the pocket's end wall and to the rail's inner end; - the joints: q1..q6 (read from the solver empties, i.e. what the drivers computed) inside design.json's limits; the J3 cylinder's length, the piston's end inside the barrel's bore (bore from -0.270 to +0.500 along the cylinder); - crossings: every pair of the arm's mesh objects (all of them, parent and child too), every arm object against the context (deck, both rails, end walls, hull lining, basin channel, aperture rim, docking lamps and their housings, both doors), the rail kit, the energy chain links and this version's door fixture; the cable and hose curves (evaluated, with their hooks) against the arm's objects except at their glands and ports. A crossing is a pair of triangles that really meet and each reach more than 0.2 mm through the other's plane (touching is not crossing); - clearances: for the same pairs, the smallest distance (vertices of one against the other's surface, both ways; the vertices thinned to at most 2500 per object), the minimum over the timeline and the parts where it happens. They are split into the joints' own running gaps (a link and its parent, and the declared mounts in MOUNTS: the tool on its flange, the plate on its pad, the carriage's rollers on the rail, the pinion in the rack, the chain's end on its bracket) and all other pairs; and the smallest distance per context object (a pair farther apart than NEAR is not measured and reads '> NEAR'); - the carriage's ends: at the frames where it stands at its travel ends, the strike pads' distance to the end-stop buffers, the carriage body's distance to the rail's inner end, and the whole arm's distance to the pocket's end wall (measured up to END_NEAR). """ import math import os import json import time import numpy as np import bpy from mathutils import Vector, Matrix from mathutils.bvhtree import BVHTree HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) EPS = 0.0002 NEAR = 0.20 # clearances are measured for pairs closer than this (bounding boxes) END_NEAR = 1.50 # the arm's distance to the pocket's end wall is measured up to this MOUNTS = {("tool B", "door fixture B"), ("carriage", "rail kit"), ("carriage", "upper rail"), ("carriage", "rail beam"), ("carriage pinion", "rail beam"), ("carriage pinion", "rail kit"), ("carriage", "chain link 00"), ("tool A hub", "flange"), ("tool B", "flange"), ("J3 piston", "upper arm"), # the rod eye on the knob pin # version A's hooks on the knob's neck (fix round, CRITIQUE_2 should 7): a designed contact, checked by # grip_contact (0-1 mm, never crossing) ("finger top distal", "door fixture A"), ("finger left distal", "door fixture A"), ("finger bottom distal", "door fixture A"), ("finger right distal", "door fixture A")} CTX_PREFIX = ("Moonpool | deck around open well", "Pocket AFT | upper rail", "Pocket FORE | upper rail", "Pocket AFT | conformal closed end", "Pocket FORE | conformal closed end", "Moonpool | hull lining", "Moonpool | curved dry basin channel", "Moonpool | titanium aperture rim", "Moonpool | docking lamp", "Moonpool | recessed docking lamp housing", "Moonpool_Door_Aft", "Moonpool_Door_Fore") CURVE_SEATS = {"U08", "U08g", "U08h", "L17", "R04p", "R01", "L03.L", "L03.R", "L04b.L", "L04b.R", "L14.L", "L04s.L", "L04r.L", "L12.L"} with open(os.path.join(ROOT, "design.json")) as _f: _D0 = json.load(_f) D_T = {"A": _D0["tools"]["A"]["d_t"], "B": _D0["tools"]["B"]["d_t"]} BORE = tuple(_D0["kinematics"]["rod"].get("bore", (-0.270, 0.500))) # the bore floor and the gland, behind / ahead of the trunnion ROD = _D0["kinematics"]["rod"].get("rod_len", 0.800) # rod eye to the piston _SC = BORE[1] / 0.500 # the scale-up def design(): with open(os.path.join(ROOT, "design.json")) as f: return json.load(f) class Geo: """an object's triangles in its own frame (numpy) and its part ids; world copies per frame""" def __init__(self, ob, me=None, parts_from=None): self.ob = ob me = me or ob.data n = len(me.vertices) co = np.empty(n * 3, dtype=np.float64) 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) tp = np.empty(nt, dtype=np.int64) me.loop_triangles.foreach_get("polygon_index", tp) lay = me.attributes.get('part') ids = list(me.get("parts", [])) if parts_from is None else parts_from dec = list(me.get("decals", [])) if parts_from is None else [] if lay is not None and ids: pv = np.empty(len(me.polygons), dtype=np.int64) lay.data.foreach_get("value", pv) tpart = pv[tp] else: tpart = np.full(nt, -1, dtype=np.int64) keep = np.ones(nt, dtype=bool) if dec: decal_ids = np.array([i for i, d in enumerate(dec) if d], dtype=np.int64) if len(decal_ids): keep &= ~np.isin(tpart, decal_ids) self.tris = tv.reshape(nt, 3)[keep] self.tpart = tpart[keep] self.ids = ids self.tris_list = [tuple(t) for t in self.tris.tolist()] used = np.unique(self.tris.reshape(-1)) if len(self.tris) else np.arange(n) step = max(1, len(used) // 2500) self.sample_idx = used[::step] self.vpart = np.full(n, -1, dtype=np.int64) for k in range(3): self.vpart[self.tris[:, k]] = self.tpart self.world = None self.tree = None self.lo = self.hi = None def vpart_name(self, vi): k = int(self.vpart[vi]) return self.ids[k] if 0 <= k < len(self.ids) else self.ob.name.split(" | ")[-1] def part(self, ti): k = int(self.tpart[ti]) return self.ids[k] if 0 <= k < len(self.ids) else self.ob.name.split(" | ")[-1] def update(self, M): A = np.array(M, dtype=np.float64) w = self.local @ A[:3, :3].T + A[:3, 3] self.world = w self.tree = None used = w[self.tris.reshape(-1)] if len(self.tris) else w self.lo = used.min(axis=0) self.hi = used.max(axis=0) def bvh(self): if self.tree is None: self.tree = BVHTree.FromPolygons(self.world.tolist(), self.tris_list, all_triangles=True) return self.tree def gap_boxes(a, b): d = np.maximum(0.0, np.maximum(a.lo - b.hi, b.lo - a.hi)) return float(np.sqrt((d * d).sum())) def _straddles(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 crossings(a, b, ignore_a=None, ignore_b=None): """[(part a, part b)] of the triangle pairs that really cross""" if gap_boxes(a, b) > 0.001: return [] out = [] for i, j in a.bvh().overlap(b.bvh()): pa, pb = a.part(i), b.part(j) if ignore_a and pa in ignore_a: continue if ignore_b and pb in ignore_b: continue P = [Vector(a.world[v]) for v in a.tris[i]] Q = [Vector(b.world[v]) for v in b.tris[j]] if _straddles(P, Q) and _straddles(Q, P): out.append((pa, pb)) return out def clearance(a, b, ignore_a=None, ignore_b=None, near=NEAR): """[distance, part of a, part of b]: the smallest vertex-to-surface distance both ways (thinned vertices)""" if gap_boxes(a, b) > near: return None best = [near, None, None] for x, y, ix, iy, flip in ((a, b, ignore_a, ignore_b, False), (b, a, ignore_b, ignore_a, True)): ty = y.bvh() lo, hi = y.lo - best[0], y.hi + best[0] idx = x.sample_idx pts = x.world[idx] m = np.all((pts >= lo) & (pts <= hi), axis=1) for vi in idx[m]: px = x.vpart_name(vi) if ix and px in ix: continue hit = ty.find_nearest(Vector(x.world[vi]), best[0]) if hit[0] is None: continue py = y.part(hit[2]) if iy and py in iy: continue if hit[3] < best[0]: best = [hit[3], py if flip else px, px if flip else py] return best if best[1] is not None else None def part_x(g, prefix): """world x range of the vertices of one part family (ids starting with prefix)""" ids = [k for k, nm in enumerate(g.ids) if nm.startswith(prefix)] m = np.isin(g.vpart, ids) if not m.any(): return None xs = g.world[m][:, 0] return float(xs.min()), float(xs.max()) def short(name): return name.split(" | ")[-1] def is_joint_pair(na, nb, parents): """a link and its parent (the joint's own running gap), or a declared mount""" a, b = short(na), short(nb) if parents.get(a) == b or parents.get(b) == a: return True return (a, b) in MOUNTS or (b, a) in MOUNTS def curve_geo(ob, dg): ev = ob.evaluated_get(dg) me = ev.to_mesh() g = Geo(ob, me=me, parts_from=[]) ev.to_mesh_clear() g.ids = [ob.name.split(" | ")[-1]] return g def run(sf, step=1, only=None): t0 = time.time() import dra_rigdata as rd parents = {l["name"]: l["parent"] for l in rd.LINKS} D = design() K = D["kinematics"] lim = {k: [math.radians(a) for a in K["joints"][k]["limits"]] for k in K["joints"]} ctl = bpy.data.objects["DoorArm | CONTROLS"] vers = [v for v in (only.split(",") if only else ["A", "B"]) if v in ("A", "B")] frames = list(range(1, 121, step)) if 120 not in frames: frames.append(120) ctx_obs = [o for o in sf.objects if o.type == 'MESH' and o.name.startswith(CTX_PREFIX) and not o.hide_render] ctx = {o.name: Geo(o) for o in ctx_obs} kit_obs = [o for o in sf.objects if o.type == 'MESH' and (o.name.endswith(("| rail kit", "| rail beam")) or "| chain link" in o.name)] kit = {o.name: Geo(o) for o in kit_obs} rep = {"frames": [frames[0], frames[-1], step], "context_objects": sorted(ctx), "near_m": NEAR, "arms": {}, "notes": ["crossing = two triangles that meet and each reach more than %.1f mm through the other's plane" % (EPS * 1000), "joint gaps = a link and its parent link, or a declared mount (MOUNTS): running clearances by design", "a clearance shown as '> %.2f' was never measured closer than that" % NEAR]} for ver in vers: ctl["version"] = 0 if ver == "A" else 1 ctl.update_tag() sf.frame_set(frames[0]) for key in ("AFT", "FORE"): prefix = "DoorArm_%s_%s" % (key, ver) arm_obs = [o for o in sf.objects if o.name.startswith(prefix + " |") and o.type == 'MESH'] curves = [o for o in sf.objects if o.name.startswith(prefix + " |") and o.type == 'CURVE'] geo = {o.name: Geo(o) for o in arm_obs} fix = bpy.data.objects.get("DoorArm_%s | door fixture %s" % (key, ver)) others = dict(ctx) others.update({n: g for n, g in kit.items() if n.startswith("DoorArm_%s |" % key)}) if fix is not None: others[fix.name] = Geo(fix) door = bpy.data.objects[D["doors"][key]["door"]] dr = D["doors"][key] taw = dr.get("tool_axes_world_" + ver, dr["tool_axes_world"]) xt = Vector(taw["x_t"]) Rt = Matrix((taw["x_t"], taw["y_t"], taw["z_t"])).transposed() att = Vector(dr["attach_local"]) c_lo, c_hi = sorted(D["carriage"]["travel_ends"][key]) ar = {"pose_err_mm": 0.0, "pose_err_deg": 0.0, "joints_deg": {}, "limit_breaks": [], "carriage": [99, -99], "carriage_ok": True, "cyl_L": [9, -9], "piston_end": [9, -9], "piston_ok": True, "crossings": {}, "crossing_frames": {}, "clear": {}, "curve_crossings": {}, "objects": len(arm_obs), "curves": len(curves)} solv = {n: bpy.data.objects["%s | IK%d %s" % (prefix, i, n2)] for i, n, n2 in ((2, "q1", "yaw"), (3, "q2", "shoulder"), (4, "q3", "elbow"), (6, "q4", "wrist angles"))} ik1 = bpy.data.objects["%s | IK1 target" % prefix] ik6 = bpy.data.objects["%s | IK6 wrist angles" % prefix] names = sorted(geo) car = geo.get("%s | carriage" % prefix) kitg = others.get("DoorArm_%s | rail kit" % key) wall = next((g for n, g in others.items() if n.startswith("Pocket %s | conformal closed end" % key)), None) rail = others.get("DoorArm_%s | rail beam" % key) or next( (g for n, g in others.items() if n.startswith("Pocket %s | upper rail" % key)), None) ends = {"pad_to_buffer_mm": [], "body_to_rail_inner_end_mm": None, "arm_to_end_wall_m": None} inner_sign = 1.0 if key == "AFT" else -1.0 # the rail's inner end is toward +x (AFT) / -x (FORE) for f in frames: sf.frame_set(f) dg = bpy.context.evaluated_depsgraph_get() for n, g in geo.items(): g.update(g.ob.evaluated_get(dg).matrix_world) for n, g in others.items(): g.update(g.ob.evaluated_get(dg).matrix_world) # pose fl = bpy.data.objects["%s | flange" % prefix].evaluated_get(dg).matrix_world dl = door.evaluated_get(dg).matrix_world.translation P = dl + att WCt = P - xt * D_T[ver] e_mm = (fl.translation - WCt).length * 1000 tr = fl.to_3x3().normalized().transposed() @ Rt e_deg = math.degrees(math.acos(max(-1.0, min(1.0, (tr[0][0] + tr[1][1] + tr[2][2] - 1) / 2)))) ar["pose_err_mm"] = max(ar["pose_err_mm"], e_mm) ar["pose_err_deg"] = max(ar["pose_err_deg"], e_deg) # joints (as the drivers computed them) e1 = ik1.evaluated_get(dg) c = e1["c"] ar["carriage"] = [min(ar["carriage"][0], c), max(ar["carriage"][1], c)] if not (c_lo - 1e-4 <= c <= c_hi + 1e-4): ar["carriage_ok"] = False e6 = ik6.evaluated_get(dg) q = {"J1": solv["q1"].evaluated_get(dg)["q1"], "J2": solv["q2"].evaluated_get(dg)["q2"], "J3": solv["q3"].evaluated_get(dg)["q3"], "J4": e6["q4"], "J5": e6["q5"], "J6": e6["q6"]} for J, v in q.items(): r = ar["joints_deg"].setdefault(J, [999, -999]) r[0] = min(r[0], math.degrees(v)) r[1] = max(r[1], math.degrees(v)) if not (lim[J][0] <= v <= lim[J][1]): ar["limit_breaks"].append([f, J, round(math.degrees(v), 2)]) L = e6["L"] ar["cyl_L"] = [min(ar["cyl_L"][0], L), max(ar["cyl_L"][1], L)] pe = L - ROD ar["piston_end"] = [min(ar["piston_end"][0], pe), max(ar["piston_end"][1], pe)] if not (BORE[0] + 0.005 * _SC <= pe <= BORE[1] - 0.04 * _SC): ar["piston_ok"] = False # the carriage at its travel ends: strike pads to the buffers, body to the rail's inner end, arm to # the pocket's end wall at_inner = abs(c - (c_hi if key == "AFT" else c_lo)) < 1e-4 at_wall = abs(c - (c_lo if key == "AFT" else c_hi)) < 1e-4 if car is not None and kitg is not None and (at_inner or at_wall): pads = part_x(car, "C12") bm = [k for k, nm in enumerate(kitg.ids) if nm.startswith("K03r")] bx = kitg.world[np.isin(kitg.vpart, bm)][:, 0] if bm else np.zeros(0) if pads and len(bx): side = inner_sign if at_inner else -inner_sign pad_face = pads[1] if side > 0 else pads[0] cand = bx[bx > pad_face - 0.002] if side > 0 else bx[bx < pad_face + 0.002] if len(cand): gap = (cand.min() - pad_face) if side > 0 else (pad_face - cand.max()) ends["pad_to_buffer_mm"].append([f, "inner end" if at_inner else "end wall side", round(gap * 1000, 2)]) if at_inner and rail is not None: body = part_x(car, "C01") rx = rail.world[:, 0] rail_end = rx.max() if key == "AFT" else rx.min() gap = (rail_end - body[1]) if key == "AFT" else (body[0] - rail_end) ends["body_to_rail_inner_end_mm"] = round(gap * 1000, 2) if at_wall and wall is not None: best = ends["arm_to_end_wall_m"] for n, g in geo.items(): cl = clearance(g, wall, near=END_NEAR) if cl and (best is None or cl[0] < best[0]): best = [round(cl[0], 4), f, short(n), cl[1]] ends["arm_to_end_wall_m"] = best # crossings and clearances: the arm's objects with each other for i, na in enumerate(names): for nb in names[i + 1:]: cr = crossings(geo[na], geo[nb]) k_ = "%s x %s" % (short(na), short(nb)) if cr: ar["crossings"][k_] = ar["crossings"].get(k_, 0) + len(cr) ar["crossing_frames"].setdefault(k_, []).append([f, sorted(set("%s x %s" % p for p in cr))[:6]]) cl = clearance(geo[na], geo[nb]) if cl: cur = ar["clear"].get(k_) if cur is None or cl[0] < cur[0]: ar["clear"][k_] = [round(cl[0], 4), f, cl[1], cl[2]] # the arm against the context, the rail kit, the chain and its fixture for na in names: for nb, gb in others.items(): cr = crossings(geo[na], gb) k_ = "%s x %s" % (short(na), nb) if cr: ar["crossings"][k_] = ar["crossings"].get(k_, 0) + len(cr) ar["crossing_frames"].setdefault(k_, []).append([f, sorted(set("%s x %s" % p for p in cr))[:6]]) cl = clearance(geo[na], gb) if cl: cur = ar["clear"].get(k_) if cur is None or cl[0] < cur[0]: ar["clear"][k_] = [round(cl[0], 4), f, cl[1], cl[2]] # the curves against the arm (except at their glands / ports) and against the context for cu in curves: gc = curve_geo(cu, dg) gc.update(cu.evaluated_get(dg).matrix_world) targets = [(short(na), geo[na], CURVE_SEATS) for na in names] + \ [(nb, gb, None) for nb, gb in others.items()] for tn, gb, ign in targets: cr = crossings(gc, gb, ignore_b=ign) k_ = "%s x %s" % (short(cu.name), tn) if cr: ar["curve_crossings"][k_] = ar["curve_crossings"].get(k_, 0) + len(cr) ar["crossing_frames"].setdefault(k_, []).append([f, sorted(set("%s x %s" % p for p in cr))[:6]]) cl = clearance(gc, gb, ignore_b=ign) if cl: cur = ar["clear"].get(k_) if cur is None or cl[0] < cur[0]: ar["clear"][k_] = [round(cl[0], 4), f, cl[1], cl[2]] ar["carriage"] = [round(x, 4) for x in ar["carriage"]] ar["pose_err_mm"] = round(ar["pose_err_mm"], 4) ar["pose_err_deg"] = round(ar["pose_err_deg"], 4) ar["joints_deg"] = {k: [round(a, 2), round(b, 2)] for k, (a, b) in ar["joints_deg"].items()} ar["cyl_L"] = [round(x, 4) for x in ar["cyl_L"]] ar["piston_end"] = [round(x, 4) for x in ar["piston_end"]] pads = [x[2] for x in ends["pad_to_buffer_mm"]] ends["pad_to_buffer_min_mm"] = min(pads) if pads else None cond = {} for f_, where, mm in ends["pad_to_buffer_mm"]: e_ = cond.setdefault(where, {"frames": [], "min_mm": mm, "max_mm": mm}) e_["frames"].append(f_) e_["min_mm"], e_["max_mm"] = min(e_["min_mm"], mm), max(e_["max_mm"], mm) for e_ in cond.values(): fr = e_["frames"] e_["frames"] = "%d frames (%d-%d)" % (len(fr), fr[0], fr[-1]) ends["pad_to_buffer_mm"] = cond ar["carriage_ends"] = ends arm_short = set(short(n) for n in names) cl_sorted = sorted(ar["clear"].items(), key=lambda kv: kv[1][0]) joint_gaps, other, ctx_min = [], [], {} for k_, v_ in cl_sorted: a_, b_ = k_.split(" x ") if is_joint_pair(a_, b_, parents): joint_gaps.append([k_, v_]) else: other.append([k_, v_]) if b_ in ctx and (b_ not in ctx_min or v_[0] < ctx_min[b_][0]): ctx_min[b_] = [v_[0], v_[1], a_, v_[2], v_[3]] ar["clear_joint_gaps_lowest"] = joint_gaps[:25] ar["clear_other_lowest"] = other[:40] ar["clear_by_context_object"] = {n: (ctx_min[n] if n in ctx_min else "> %.2f" % NEAR) for n in sorted(ctx)} del ar["clear"] rep["arms"]["%s_%s" % (key, ver)] = ar print("[dra] checked", key, ver, "%.1fs" % (time.time() - t0), flush=True) ctl["version"] = 0 ctl.update_tag() sf.frame_set(1) rep["chain"] = {key: chain_check(sf, D, key, frames, kit) for key in ("AFT", "FORE")} rep["fixtures_follow_door_mm"] = fixture_check(sf, D, frames) rep["grip_contact"] = grip_contact(sf, frames) rep["version_switch"] = switch_check(sf, ctl) rep["drivers"] = driver_check() rep["summary"] = {k: dict(pose_err_mm=a["pose_err_mm"], pose_err_deg=a["pose_err_deg"], limit_breaks=len(a["limit_breaks"]), carriage=a["carriage"], carriage_ok=a["carriage_ok"], piston_ok=a["piston_ok"], piston_end=a["piston_end"], crossing_pairs=len(a["crossings"]), crossing_tris=sum(a["crossings"].values()), curve_crossing_pairs=len(a["curve_crossings"]), pad_to_buffer_min_mm=a["carriage_ends"]["pad_to_buffer_min_mm"], body_to_rail_inner_end_mm=a["carriage_ends"]["body_to_rail_inner_end_mm"], arm_to_end_wall_m=a["carriage_ends"]["arm_to_end_wall_m"], lowest_joint_gap=a["clear_joint_gaps_lowest"][0] if a["clear_joint_gaps_lowest"] else None, lowest_other=a["clear_other_lowest"][0] if a["clear_other_lowest"] else None) for k, a in rep["arms"].items()} rep["summary"]["chain"] = {k: dict(crossings=c["crossings"], spacing_error_mm=c["spacing_error_mm"], lowest_to_tray=c["lowest_to_rail_kit"]) for k, c in rep["chain"].items()} rep["summary"]["fixtures_follow_door_mm"] = rep["fixtures_follow_door_mm"] rep["summary"]["grip_contact_mm"] = {k: dict(min=v["min_mm"], max=v["max_mm"], crossing_frames=v["crossing_frames"]) for k, v in rep["grip_contact"].items()} rep["summary"]["version_switch_ok"] = rep["version_switch"]["ok"] rep["summary"]["drivers"] = {k: rep["drivers"][k] for k in ("count", "simple_and_valid", "not_simple_or_invalid")} rep["seconds"] = round(time.time() - t0, 1) return rep def chain_check(sf, D, key, frames, kit): """the energy chain of one door at every frame: crossings between links (each with its next two), between the links and the rail kit (tray, flanges), the spacing of neighbouring links' centres against the pitch, and the links' lowest clearance to the rail kit""" pitch = D["energy_chain"][key]["pitch"] links = sorted([n for n in kit if n.startswith("DoorArm_%s | chain link" % key)]) kg = kit.get("DoorArm_%s | rail kit" % key) out = {"links": len(links), "crossings": 0, "crossing_frames": [], "spacing_error_mm": 0.0, "lowest_to_rail_kit": None} for f in frames: sf.frame_set(f) dg = bpy.context.evaluated_depsgraph_get() for n in links: kit[n].update(kit[n].ob.evaluated_get(dg).matrix_world) if kg is not None: kg.update(kg.ob.evaluated_get(dg).matrix_world) for i, n in enumerate(links): g = kit[n] for m in links[i + 1:i + 3]: cr = crossings(g, kit[m]) if cr: out["crossings"] += len(cr) out["crossing_frames"].append([f, short(n), short(m)]) if i + 1 < len(links): d = (kit[links[i + 1]].ob.evaluated_get(dg).matrix_world.translation - g.ob.evaluated_get(dg).matrix_world.translation).length out["spacing_error_mm"] = max(out["spacing_error_mm"], abs(d - pitch) * 1000) if kg is not None: cr = crossings(g, kg) if cr: out["crossings"] += len(cr) out["crossing_frames"].append([f, short(n), "rail kit"]) cl = clearance(g, kg) if cl and (out["lowest_to_rail_kit"] is None or cl[0] < out["lowest_to_rail_kit"][0]): out["lowest_to_rail_kit"] = [round(cl[0], 4), f, short(n), cl[1], cl[2]] out["spacing_error_mm"] = round(out["spacing_error_mm"], 3) out["crossing_frames"] = out["crossing_frames"][:20] sf.frame_set(1) return out def fixture_check(sf, D, frames): """every door fixture follows its door: location = door location + attach offset, rotation 0 (the door never turns). Checked with its own version switched on (a hidden object is not evaluated, so it waits where it was)""" worst = {} ctl = bpy.data.objects["DoorArm | CONTROLS"] for ver in ("A", "B"): ctl["version"] = 0 if ver == "A" else 1 ctl.update_tag() for key in ("AFT", "FORE"): door = bpy.data.objects[D["doors"][key]["door"]] att = Vector(D["doors"][key]["attach_local"]) ob = bpy.data.objects.get("DoorArm_%s | door fixture %s" % (key, ver)) if ob is None: continue e, r = 0.0, 0.0 for f in frames: sf.frame_set(f) dg = bpy.context.evaluated_depsgraph_get() M = ob.evaluated_get(dg).matrix_world dl = door.evaluated_get(dg).matrix_world.translation e = max(e, (M.translation - (dl + att)).length * 1000) r = max(r, math.degrees(M.to_quaternion().angle)) worst["%s %s" % (key, ver)] = {"position_mm": round(e, 4), "rotation_deg": round(r, 4)} ctl["version"] = 0 ctl.update_tag() sf.frame_set(1) return worst def grip_contact(sf, frames): """version A with the grip as the rig has it: per frame, the surface gap from each hook (finger distal) to its door's knob (door fixture A), measured from points spread over the hook's faces near the knob (not only its corners); a point inside the knob counts as a crossing. Wanted: 0-1 mm at every frame, no crossing.""" from mathutils.bvhtree import BVHTree ctl = bpy.data.objects["DoorArm | CONTROLS"] ctl["version"] = 0 ctl.update_tag() out = {} for key in ("AFT", "FORE"): fx = bpy.data.objects.get("DoorArm_%s | door fixture A" % key) if fx is None: continue fingers = [bpy.data.objects["DoorArm_%s_A | finger %s distal" % (key, w)] for w in ("top", "left", "bottom", "right")] lo, hi, bad, per = 9.0, 0.0, [], {} # the faces of each hook that come near the knob (at frame 1), and points spread over them sf.frame_set(frames[0]) dg = bpy.context.evaluated_depsgraph_get() Mf = fx.evaluated_get(dg).matrix_world bvh0 = BVHTree.FromPolygons([Mf @ v.co for v in fx.data.vertices], [tuple(p.vertices) for p in fx.data.polygons]) samples = {} for ob in fingers: me = ob.data me.calc_loop_triangles() M = ob.evaluated_get(dg).matrix_world pts = [] for lt in me.loop_triangles: cs = [me.vertices[i].co for i in lt.vertices] if min(bvh0.find_nearest(M @ c)[3] or 9.0 for c in cs) > 0.02: continue for i in range(7): for j in range(7 - i): pts.append(cs[0] + (cs[1] - cs[0]) * (i / 6) + (cs[2] - cs[0]) * (j / 6)) samples[ob.name] = pts or [v.co for v in me.vertices] for f in frames: sf.frame_set(f) dg = bpy.context.evaluated_depsgraph_get() Mf = fx.evaluated_get(dg).matrix_world fv = [Mf @ v.co for v in fx.data.vertices] bvh = BVHTree.FromPolygons(fv, [tuple(p.vertices) for p in fx.data.polygons]) for ob in fingers: M = ob.evaluated_get(dg).matrix_world g, inside = 9.0, False for c in samples[ob.name]: p = M @ c loc, nrm, _, dist = bvh.find_nearest(p) if loc is None: continue if (p - loc).dot(nrm) < -EPS: inside = True g = min(g, dist) w = ob.name.split(" | ")[-1] per[w] = min(per.get(w, 9.0), g) lo, hi = min(lo, g), max(hi, g) if inside: bad.append(f) out[key] = dict(min_mm=round(lo * 1000, 3), max_mm=round(hi * 1000, 3), crossing_frames=sorted(set(bad)), per_hook_min_mm={k: round(v * 1000, 3) for k, v in per.items()}) sf.frame_set(1) return out def switch_check(sf, ctl): """the version switch: with version 0 every A object (arms, curves, fixtures) shows and every B object is hidden, in the viewport and the render; with version 1 the other way round""" obs = [o for o in sf.objects if o.get("version") in ("A", "B")] res = {"objects": len(obs), "ok": True, "wrong": []} for v in (0, 1, 0): # A, B and back to A (a hidden object must come back) ctl["version"] = v ctl.update_tag() sf.frame_set(1) bpy.context.view_layer.update() for o in obs: hidden = o.get("version") != ("A" if v == 0 else "B") if bool(o.hide_viewport) != hidden or bool(o.hide_render) != hidden: res["ok"] = False res["wrong"].append([v, o.name, bool(o.hide_viewport), bool(o.hide_render)]) ctl["version"] = 0 ctl.update_tag() sf.frame_set(1) res["wrong"] = res["wrong"][:20] return res def driver_check(): """every driver in the file: a simple expression (runs without Python) and valid""" n = 0 bad = [] for ob in bpy.data.objects: ad = ob.animation_data if ad is None: continue for fc in ad.drivers: n += 1 d = fc.driver if not (d.type == 'SCRIPTED' and d.is_simple_expression and d.is_valid): bad.append([ob.name, fc.data_path, fc.array_index, d.expression, bool(d.is_simple_expression), bool(d.is_valid)]) return {"count": n, "simple_and_valid": n - len(bad), "not_simple_or_invalid": len(bad), "bad": bad[:20]}