""" The doors' leaves, opened and shut: do they have somewhere to go, all the way, and does the rig work? blender -b --factory-startup Anchorage_Station_exterior.blend --python build/check_doors.py [-- --travel 4.10] The kit's big door leaf (SM_Door_L / SM_Hall_Door_L, 6.8 x 4.0 m, one piece) can only lift. The user, 2026-10-14: "these doors are when opened (slide) they will clip through the walls because there is not enough coverage", and then "rig the doors so they open and close" (build/doors.py). For every leaf this reports * what it meets on its way up, 5 cm at a time: the frame it rides in (the kit seats the shut leaf in it) is allowed within the faces the shut leaf already meets; anything else is a fault (doors.sweep) * whether any of it shows where it should not, at eleven heights of its travel: points on its two faces higher than the kit's head slot, seen (a) from outside, by rays from the point that leave the station without meeting a face, and (b) from where people stand on either side of the doorway (eyes 1 to 9 m away, 1.0 to 2.6 m up, every eye that sees the doorway's middle). Points in the head slot itself (from the opening's top to the top of the frame's faces the shut leaf stands in) are counted apart: looking straight up into the slot one sees the leaf in it, which is the kit's door. The eyes found what the rays did not: a slit at the outer doors' heads (sealed since, exterior.door_casing) * the doorway's clear height (how far the leaf has to lift) * the rig: the driver works without Python, its slider opens the leaf 0 / 25 / 50 / 100 %, and the timeline shuts, opens and shuts it again The leaves are first stood shut whatever the sliders say (doors.at_rest). Nothing in the file is changed. """ import math import os import sys import bpy from mathutils import Vector from mathutils.bvhtree import BVHTree sys.dont_write_bytecode = True sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import doors # noqa: E402 import exterior as ext # noqa: E402 ARGS = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] TRAVEL = float(ARGS[ARGS.index("--travel") + 1]) if "--travel" in ARGS else None LIFTS = (0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 3.75, 4.0) # and the full travel REGION = 14.0 # eyes and near obstacles: this far round a door sc = bpy.context.scene station = bpy.data.collections["ANCHORAGE STATION"] shut = doors.at_rest() def box(ob): c = [ob.matrix_world @ Vector(k) for k in ob.bound_box] return (Vector((min(p.x for p in c), min(p.y for p in c), min(p.z for p in c))), Vector((max(p.x for p in c), max(p.y for p in c), max(p.z for p in c)))) def tris(ob): me = ob.data if hasattr(me, "calc_loop_triangles"): me.calc_loop_triangles() mw = ob.matrix_world return [mw @ v.co for v in me.vertices], [tuple(t.vertices) for t in me.loop_triangles] obstacles = [] # everything of the station a leaf can be hidden behind (no leaves, no decals) for ob in station.all_objects: if ob.type == "MESH" and ob.get("ue_decal_material") is None and ob.get("ue_role") != "door_leaf" and len(ob.data.polygons): lo, hi = box(ob) obstacles.append((ob, lo, hi)) FAR = {} def far_bvh(ob): if ob.name not in FAR: v, f = tris(ob) FAR[ob.name] = BVHTree.FromPolygons(v, f) return FAR[ob.name] def region_of(c): """One BVH of every obstacle whose box comes within REGION of c, and the list of the others.""" verts, faces, inside, outside = [], [], [], [] for ob, lo, hi in obstacles: if hi.x < c.x - REGION or lo.x > c.x + REGION or hi.y < c.y - REGION or lo.y > c.y + REGION or hi.z < -3.0 or lo.z > 14.0: outside.append((ob, lo, hi)) continue v, f = tris(ob) base = len(verts) verts.extend(v) faces.extend((a + base, b + base, d + base) for a, b, d in f) inside.append(ob) return BVHTree.FromPolygons(verts, faces), outside def escapes(near, outside, start, d, reach=200.0): """A ray from start along d that meets no face of the station within reach.""" if near.ray_cast(start, d, reach)[0] is not None: return False end = start + d * reach rlo = Vector((min(start.x, end.x), min(start.y, end.y), min(start.z, end.z))) rhi = Vector((max(start.x, end.x), max(start.y, end.y), max(start.z, end.z))) for ob, lo, hi in outside: if hi.x < rlo.x or lo.x > rhi.x or hi.y < rlo.y or lo.y > rhi.y or hi.z < rlo.z or lo.z > rhi.z: continue if far_bvh(ob).ray_cast(start, d, reach)[0] is not None: return False return True def sees(near, eye, p): d = p - eye n = d.length return near.ray_cast(eye, d / n, n - 0.02)[0] is None DIRS = [Vector((math.cos(math.radians(k * 30.0)) * math.cos(math.radians(e)), math.sin(math.radians(k * 30.0)) * math.cos(math.radians(e)), math.sin(math.radians(e)))) for k in range(12) for e in (0.0, 25.0, 55.0)] + [Vector((0.0, 0.0, 1.0))] faults = 0 leaves = ext.door_leaves() for leaf, lo, hi, thin in leaves: travel = TRAVEL if TRAVEL is not None else float(leaf.get("ue_door_travel_m", (0.0, 0.0, ext.DOOR_TRAVEL))[2]) axis = Vector((1.0, 0.0, 0.0)) if thin == "x" else Vector((0.0, 1.0, 0.0)) c = (lo + hi) / 2.0 print("[doors] %-18s %-24s shut x %.2f..%.2f y %.2f..%.2f z %.2f..%.2f; travel %.2f m: open z %.2f..%.2f" % ( leaf.name, doors.NAMES.get(leaf.name, ""), lo.x, hi.x, lo.y, hi.y, lo.z, hi.z, travel, lo.z + travel, hi.z + travel)) # 1. what it meets on the way seat, met = doors.sweep(leaf, travel) print("[doors] rides in %s (the kit seats it there; faces between z %s)" % ( ", ".join(sorted(seat)) or "nothing", ", ".join("%.2f..%.2f" % seat[k] for k in sorted(seat)))) for name, (mesh, t0, t1, z0, z1) in sorted(met.items()): print("[doors] MEETS %-34s (%s) from %.2f to %.2f m of lift, its faces at z %.2f..%.2f" % (name, mesh, t0, t1, z0, z1)) faults += len(met) if not met: print("[doors] meets nothing else from shut to %.2f m (%d steps of %.0f cm)" % (travel, int(round(travel / doors.STEP)), doors.STEP * 100)) # 2. the doorway's clear height, and what shows on the way near_col = doors.around(lo, hi, 0.0) top = doors.opening_top(leaf, near_col) head = top if top is not None else hi.z print("[doors] the doorway is clear up to z %.2f at its middle: the leaf has to lift %.2f m, it lifts %.2f" % ( head, head - lo.z, travel)) near, outside = region_of(c) eyes = [] for s in (1.0, -1.0): for dist in (1.0, 2.5, 5.0, 9.0): for lat in (-2.6, 0.0, 2.6): for up in (1.0, 1.7, 2.6): along = Vector((0.0, 1.0, 0.0)) if thin == "x" else Vector((1.0, 0.0, 0.0)) e = Vector((c.x, c.y, lo.z + up)) + axis * (s * (dist + (hi - lo).dot(axis) / 2.0)) + along * lat if sees(near, e, Vector((c.x, c.y, lo.z + 1.5))): eyes.append((e, s)) print("[doors] %d eyes see the doorway (%d on the +%s side, %d on the other)" % ( len(eyes), sum(1 for e, s in eyes if s > 0), thin, sum(1 for e, s in eyes if s < 0))) # looking straight up into the kit's head slot one sees the leaf in it: that is the kit's door. Above the slot # (the top of the frame's faces the shut leaf stands in) nothing of the leaf may show. slot_top = max([z1 for z0, z1 in seat.values()] + [head]) shown_out, shown_eye, tested, examples, in_slot, slot_seen = 0, 0, 0, [], 0, 0 for t in LIFTS + (travel,): if t > travel + 1e-6: continue zs = [lo.z + t + 0.05 + 0.25 * k for k in range(17)] + [hi.z + t - 0.05] for a in (0.03, 0.2, 0.4, 0.6, 0.8, 0.97): for z in zs: if z <= head + 0.03 or z > hi.z + t: continue for s in (1.0, -1.0): face = (hi if s > 0 else lo).dot(axis) if thin == "x": p = Vector((face, lo.y + (hi.y - lo.y) * a, z)) else: p = Vector((lo.x + (hi.x - lo.x) * a, face, z)) n = axis * s out = any(escapes(near, outside, p + d * 0.06, d) for d in DIRS if d.dot(n) >= -0.01) seen = next((e for e, es in eyes if es == s and sees(near, e, p)), None) if z <= slot_top + 0.02: in_slot += 1 slot_seen += 1 if (out or seen is not None) else 0 continue tested += 1 if out: shown_out += 1 if seen is not None: shown_eye += 1 if out or seen is not None: examples.append((t, p.copy(), s, out, seen.copy() if seen is not None else None)) print("[doors] on its way up (%s m and %.2f): %d points above the kit's head slot (z %.2f) tested; %d show from " "outside, %d to someone at the doorway. In the slot (z %.2f..%.2f): %d points, %d seen looking up into it" % ( "/".join("%g" % t for t in LIFTS if t <= travel), travel, tested, slot_top, shown_out, shown_eye, head, slot_top, in_slot, slot_seen)) by = {} for t, p, s, out, seen in examples: k = (round(t, 2), "+" if s > 0 else "-") by.setdefault(k, []).append((p, out, seen)) for (t, s), rows in sorted(by.items()): zs = [p.z for p, o, e in rows] al = [p.dot(Vector((0.0, 1.0, 0.0)) if thin == "x" else Vector((1.0, 0.0, 0.0))) for p, o, e in rows] ez = [e for p, o, e in rows if e is not None] print("[doors] SHOWS at lift %.2f on its %s%s face: %d points, z %.2f..%.2f, along %.2f..%.2f; %d from outside%s" % ( t, s, thin, len(rows), min(zs), max(zs), min(al), max(al), sum(1 for p, o, e in rows if o), "" if not ez else "; eyes e.g. (%.1f, %.1f, %.1f)" % tuple(ez[0]))) faults += shown_out + shown_eye # 3. the rig fc = doors._height_driver(leaf) ctrl = bpy.data.objects.get(leaf.get("ue_door_control", "")) master = bpy.data.objects.get(doors.MASTER) if fc is None or ctrl is None or master is None: print("[doors] NOT RIGGED (no driver on its height, or no control)") faults += 1 continue drv = fc.driver fc.mute = False z0 = float(leaf["closed_location"][2]) wrong = [] got = [] for v in (0.0, 0.25, 0.5, 1.0): ctrl["open"] = v ctrl.update_tag() sc.frame_set(1) z = leaf.matrix_world.translation.z got.append("%g: %.3f" % (v, z)) if abs(z - (z0 + travel * v)) > 0.001: wrong.append("slider %g gives z %.3f, not %.3f" % (v, z, z0 + travel * v)) ctrl["open"] = 0.0 ctrl.update_tag() tl = [] for fr in (1, 31, 61, 91, 121, 151, 181): sc.frame_set(fr) tl.append("%d: %.2f" % (fr, leaf.matrix_world.translation.z - z0)) if abs(leaf.matrix_world.translation.z - z0) > 0.001: wrong.append("the timeline does not shut it again") sc.frame_set(1) fc.mute = True leaf.location = Vector(leaf["closed_location"]) print("[doors] rig: control '%s'; driver %s (%s); slider -> z %s; timeline frame: lift %s%s" % ( ctrl.name, drv.expression, "no Python needed" if drv.is_simple_expression else "NEEDS PYTHON", ", ".join(got), ", ".join(tl), "" if not wrong else "; WRONG: " + "; ".join(wrong))) faults += len(wrong) + (0 if drv.is_simple_expression and drv.is_valid else 1) print("[doors] %d leaves (%d stood shut first); in all %d things wrong (pieces met, points that show, rig faults)" % ( len(leaves), shut, faults))